From b861f76df0d90cd8920fc80e7f9fbd717375b380 Mon Sep 17 00:00:00 2001 From: Denis Shilovich Date: Fri, 12 Jul 2024 08:47:34 +0000 Subject: [PATCH] Release 1.6.5 (268) --- .bazelrc | 6 +- .gitmodules | 3 + .../Sources/NicegramSettingsController.swift | 4 +- Package.resolved | 22 +- Random.txt | 2 +- Telegram/SiriIntents/IntentMessages.swift | 2 +- .../AppIconLLC.appiconset/1-20.png | Bin 1905 -> 0 bytes .../AppIconLLC.appiconset/1-20@2x.png | Bin 3055 -> 0 bytes .../AppIconLLC.appiconset/1-20@3x.png | Bin 4390 -> 0 bytes .../AppIconLLC.appiconset/1-29.png | Bin 2405 -> 0 bytes .../AppIconLLC.appiconset/1-29@2x.png | Bin 4247 -> 0 bytes .../AppIconLLC.appiconset/1-29@3x.png | Bin 6444 -> 0 bytes .../AppIconLLC.appiconset/1-40.png | Bin 3055 -> 0 bytes .../AppIconLLC.appiconset/1-40@2x.png | Bin 5891 -> 0 bytes .../AppIconLLC.appiconset/1-40@3x.png | Bin 9230 -> 0 bytes .../AppIconLLC.appiconset/1-60@2x.png | Bin 9230 -> 0 bytes .../AppIconLLC.appiconset/1-60@3x.png | Bin 15938 -> 0 bytes .../AppIconLLC.appiconset/1-76.png | Bin 5561 -> 0 bytes .../AppIconLLC.appiconset/1-76@2x.png | Bin 12431 -> 0 bytes .../AppIconLLC.appiconset/1-83.5@2x.png | Bin 14187 -> 0 bytes .../AppIconLLC.appiconset/Contents.json | 112 +- .../Telegram-iOS/en.lproj/Localizable.strings | 164 + Tests/LottieMetalTest/BUILD | 3 +- .../Extensions/CGFloatExtensions.swift | 53 +- .../SoftwareLottieRenderer/BUILD | 4 +- .../SoftwareLottieRenderer.h | 11 +- .../SoftwareLottieRenderer/Sources/Canvas.h | 92 - .../Sources/CoreGraphicsCanvasImpl.h | 71 +- .../Sources/CoreGraphicsCanvasImpl.mm | 418 ++- .../Sources/NullCanvasImpl.mm | 81 - .../Sources/SkiaCanvasImpl.cpp | 308 ++ .../{NullCanvasImpl.h => SkiaCanvasImpl.h} | 47 +- .../Sources/SoftwareLottieRenderer.mm | 674 +--- .../Sources/ThorVGCanvasImpl.h | 64 - .../Sources/ThorVGCanvasImpl.mm | 288 -- .../Sources/CompareToReferenceRendering.swift | 127 +- .../Sources/ViewController.swift | 188 +- Tests/LottieMetalTest/skia/BUILD | 49 + .../skia/PublicHeaders/skia/include/OWNERS | 15 + .../skia/include/codec/SkAndroidCodec.h | 297 ++ .../skia/include/codec/SkAvifDecoder.h | 44 + .../skia/include/codec/SkBmpDecoder.h | 44 + .../skia/include/codec/SkCodec.h | 1085 +++++++ .../skia/include/codec/SkCodecAnimation.h | 61 + .../skia/include/codec/SkEncodedImageFormat.h | 33 + .../skia/include/codec/SkEncodedOrigin.h | 54 + .../skia/include/codec/SkGifDecoder.h | 44 + .../skia/include/codec/SkIcoDecoder.h | 44 + .../skia/include/codec/SkJpegDecoder.h | 44 + .../skia/include/codec/SkJpegxlDecoder.h | 44 + .../skia/include/codec/SkPixmapUtils.h | 31 + .../skia/include/codec/SkPngChunkReader.h | 45 + .../skia/include/codec/SkPngDecoder.h | 44 + .../skia/include/codec/SkRawDecoder.h | 50 + .../skia/include/codec/SkWbmpDecoder.h | 44 + .../skia/include/codec/SkWebpDecoder.h | 44 + .../PublicHeaders/skia/include/config/OWNERS | 2 + .../skia/include/config/SkUserConfig.h | 121 + .../skia/include/core/SkAlphaType.h | 45 + .../skia/include/core/SkAnnotation.h | 52 + .../PublicHeaders/skia/include/core/SkArc.h | 69 + .../skia/include/core/SkBBHFactory.h | 67 + .../skia/include/core/SkBitmap.h | 1275 ++++++++ .../skia/include/core/SkBlendMode.h | 112 + .../skia/include/core/SkBlender.h | 31 + .../skia/include/core/SkBlurTypes.h | 20 + .../skia/include/core/SkCanvas.h | 2686 +++++++++++++++ .../include/core/SkCanvasVirtualEnforcer.h | 61 + .../skia/include/core/SkCapabilities.h | 39 + .../skia/include/core/SkClipOp.h | 19 + .../PublicHeaders/skia/include/core/SkColor.h | 447 +++ .../skia/include/core/SkColorFilter.h | 156 + .../skia/include/core/SkColorPriv.h | 167 + .../skia/include/core/SkColorSpace.h | 242 ++ .../skia/include/core/SkColorTable.h | 62 + .../skia/include/core/SkColorType.h | 70 + .../skia/include/core/SkContourMeasure.h | 139 + .../skia/include/core/SkCoverageMode.h | 28 + .../skia/include/core/SkCubicMap.h | 47 + .../PublicHeaders/skia/include/core/SkData.h | 191 ++ .../skia/include/core/SkDataTable.h | 122 + .../skia/include/core/SkDocument.h | 93 + .../skia/include/core/SkDrawable.h | 178 + .../skia/include/core/SkExecutor.h | 41 + .../skia/include/core/SkFlattenable.h | 115 + .../PublicHeaders/skia/include/core/SkFont.h | 539 +++ .../skia/include/core/SkFontArguments.h | 94 + .../skia/include/core/SkFontMetrics.h | 139 + .../skia/include/core/SkFontMgr.h | 143 + .../skia/include/core/SkFontParameters.h | 42 + .../skia/include/core/SkFontStyle.h | 84 + .../skia/include/core/SkFontTypes.h | 25 + .../skia/include/core/SkGraphics.h | 169 + .../PublicHeaders/skia/include/core/SkImage.h | 948 ++++++ .../skia/include/core/SkImageFilter.h | 119 + .../skia/include/core/SkImageGenerator.h | 148 + .../skia/include/core/SkImageInfo.h | 628 ++++ .../PublicHeaders/skia/include/core/SkM44.h | 442 +++ .../skia/include/core/SkMallocPixelRef.h | 45 + .../skia/include/core/SkMaskFilter.h | 53 + .../skia/include/core/SkMatrix.h | 1997 ++++++++++++ .../PublicHeaders/skia/include/core/SkMesh.h | 429 +++ .../skia/include/core/SkMilestone.h | 9 + .../skia/include/core/SkOpenTypeSVGDecoder.h | 30 + .../skia/include/core/SkOverdrawCanvas.h | 94 + .../PublicHeaders/skia/include/core/SkPaint.h | 695 ++++ .../PublicHeaders/skia/include/core/SkPath.h | 1943 +++++++++++ .../skia/include/core/SkPathBuilder.h | 271 ++ .../skia/include/core/SkPathEffect.h | 113 + .../skia/include/core/SkPathMeasure.h | 95 + .../skia/include/core/SkPathTypes.h | 57 + .../skia/include/core/SkPathUtils.h | 42 + .../skia/include/core/SkPicture.h | 291 ++ .../skia/include/core/SkPictureRecorder.h | 115 + .../skia/include/core/SkPixelRef.h | 119 + .../skia/include/core/SkPixmap.h | 731 +++++ .../PublicHeaders/skia/include/core/SkPoint.h | 10 + .../skia/include/core/SkPoint3.h | 149 + .../PublicHeaders/skia/include/core/SkRRect.h | 516 +++ .../skia/include/core/SkRSXform.h | 71 + .../include/core/SkRasterHandleAllocator.h | 94 + .../PublicHeaders/skia/include/core/SkRect.h | 1374 ++++++++ .../skia/include/core/SkRefCnt.h | 389 +++ .../skia/include/core/SkRegion.h | 684 ++++ .../skia/include/core/SkSamplingOptions.h | 107 + .../skia/include/core/SkScalar.h | 161 + .../skia/include/core/SkSerialProcs.h | 117 + .../skia/include/core/SkShader.h | 112 + .../PublicHeaders/skia/include/core/SkSize.h | 93 + .../PublicHeaders/skia/include/core/SkSpan.h | 13 + .../skia/include/core/SkStream.h | 512 +++ .../skia/include/core/SkString.h | 293 ++ .../skia/include/core/SkStrokeRec.h | 159 + .../skia/include/core/SkSurface.h | 659 ++++ .../skia/include/core/SkSurfaceProps.h | 116 + .../skia/include/core/SkSwizzle.h | 21 + .../skia/include/core/SkTextBlob.h | 519 +++ .../include/core/SkTextureCompressionType.h | 30 + .../skia/include/core/SkTileMode.h | 41 + .../skia/include/core/SkTiledImageUtils.h | 125 + .../skia/include/core/SkTraceMemoryDump.h | 114 + .../skia/include/core/SkTypeface.h | 434 +++ .../PublicHeaders/skia/include/core/SkTypes.h | 199 ++ .../skia/include/core/SkUnPreMultiply.h | 55 + .../skia/include/core/SkVertices.h | 136 + .../skia/include/core/SkYUVAInfo.h | 308 ++ .../skia/include/core/SkYUVAPixmaps.h | 337 ++ .../include/docs/SkMultiPictureDocument.h | 53 + .../skia/include/docs/SkPDFDocument.h | 224 ++ .../skia/include/docs/SkXPSDocument.h | 27 + .../skia/include/effects/Sk1DPathEffect.h | 40 + .../skia/include/effects/Sk2DPathEffect.h | 33 + .../skia/include/effects/SkBlenders.h | 27 + .../skia/include/effects/SkBlurMaskFilter.h | 35 + .../skia/include/effects/SkColorMatrix.h | 57 + .../include/effects/SkColorMatrixFilter.h | 22 + .../skia/include/effects/SkCornerPathEffect.h | 32 + .../skia/include/effects/SkDashPathEffect.h | 43 + .../include/effects/SkDiscretePathEffect.h | 37 + .../skia/include/effects/SkGradientShader.h | 354 ++ .../include/effects/SkHighContrastFilter.h | 84 + .../skia/include/effects/SkImageFilters.h | 615 ++++ .../skia/include/effects/SkLumaColorFilter.h | 37 + .../include/effects/SkOverdrawColorFilter.h | 32 + .../include/effects/SkPerlinNoiseShader.h | 53 + .../skia/include/effects/SkRuntimeEffect.h | 517 +++ .../skia/include/effects/SkShaderMaskFilter.h | 28 + .../skia/include/effects/SkTableMaskFilter.h | 43 + .../skia/include/effects/SkTrimPathEffect.h | 45 + .../skia/include/encode/SkEncoder.h | 63 + .../PublicHeaders/skia/include/encode/SkICC.h | 36 + .../skia/include/encode/SkJpegEncoder.h | 128 + .../skia/include/encode/SkPngEncoder.h | 117 + .../skia/include/encode/SkWebpEncoder.h | 92 + .../skia/include/pathops/SkPathOps.h | 113 + .../skia/include/ports/SkCFObject.h | 180 + .../include/ports/SkFontConfigInterface.h | 112 + .../ports/SkFontMgr_FontConfigInterface.h | 20 + .../skia/include/ports/SkFontMgr_Fontations.h | 20 + .../skia/include/ports/SkFontMgr_android.h | 50 + .../skia/include/ports/SkFontMgr_data.h | 22 + .../skia/include/ports/SkFontMgr_directory.h | 21 + .../skia/include/ports/SkFontMgr_empty.h | 21 + .../skia/include/ports/SkFontMgr_fontconfig.h | 22 + .../skia/include/ports/SkFontMgr_fuchsia.h | 19 + .../skia/include/ports/SkFontMgr_mac_ct.h | 27 + .../skia/include/ports/SkImageGeneratorCG.h | 28 + .../skia/include/ports/SkImageGeneratorNDK.h | 40 + .../skia/include/ports/SkImageGeneratorWIC.h | 41 + .../include/ports/SkTypeface_fontations.h | 21 + .../skia/include/ports/SkTypeface_mac.h | 44 + .../skia/include/ports/SkTypeface_win.h | 63 + .../PublicHeaders/skia/include/private/OWNERS | 4 + .../skia/include/private/SkColorData.h | 385 +++ .../skia/include/private/SkEncodedInfo.h | 283 ++ .../skia/include/private/SkExif.h | 55 + .../skia/include/private/SkGainmapInfo.h | 140 + .../skia/include/private/SkGainmapShader.h | 54 + .../skia/include/private/SkIDChangeListener.h | 76 + .../include/private/SkJpegGainmapEncoder.h | 48 + .../include/private/SkJpegMetadataDecoder.h | 86 + .../skia/include/private/SkPathRef.h | 582 ++++ .../skia/include/private/SkSLSampleUsage.h | 85 + .../skia/include/private/SkWeakRefCnt.h | 173 + .../skia/include/private/SkXmp.h | 62 + .../skia/include/private/base/README.md | 4 + .../skia/include/private/base/SingleOwner.h | 75 + .../skia/include/private/base/SkAPI.h | 52 + .../skia/include/private/base/SkASAN.h | 56 + .../skia/include/private/base/SkAlign.h | 39 + .../include/private/base/SkAlignedStorage.h | 32 + .../skia/include/private/base/SkAnySubclass.h | 73 + .../skia/include/private/base/SkAssert.h | 202 ++ .../skia/include/private/base/SkAttributes.h | 90 + .../skia/include/private/base/SkCPUTypes.h | 25 + .../skia/include/private/base/SkContainers.h | 54 + .../skia/include/private/base/SkDebug.h | 27 + .../skia/include/private/base/SkDeque.h | 138 + .../skia/include/private/base/SkFeatures.h | 165 + .../skia/include/private/base/SkFixed.h | 143 + .../include/private/base/SkFloatingPoint.h | 184 ++ .../include/private/base/SkLoadUserConfig.h | 63 + .../skia/include/private/base/SkMacros.h | 94 + .../skia/include/private/base/SkMalloc.h | 152 + .../skia/include/private/base/SkMath.h | 77 + .../skia/include/private/base/SkMutex.h | 64 + .../skia/include/private/base/SkNoncopyable.h | 30 + .../skia/include/private/base/SkOnce.h | 55 + .../skia/include/private/base/SkPoint_impl.h | 560 ++++ .../skia/include/private/base/SkSafe32.h | 49 + .../skia/include/private/base/SkSemaphore.h | 84 + .../skia/include/private/base/SkSpan_impl.h | 131 + .../skia/include/private/base/SkTArray.h | 806 +++++ .../skia/include/private/base/SkTDArray.h | 235 ++ .../skia/include/private/base/SkTFitsIn.h | 105 + .../skia/include/private/base/SkTLogic.h | 56 + .../skia/include/private/base/SkTPin.h | 23 + .../skia/include/private/base/SkTemplates.h | 446 +++ .../private/base/SkThreadAnnotations.h | 104 + .../skia/include/private/base/SkThreadID.h | 23 + .../skia/include/private/base/SkTo.h | 39 + .../skia/include/private/base/SkTypeTraits.h | 33 + .../private/chromium/GrDeferredDisplayList.h | 120 + .../chromium/GrDeferredDisplayListRecorder.h | 62 + .../private/chromium/GrPromiseImageTexture.h | 43 + .../chromium/GrSurfaceCharacterization.h | 211 ++ .../chromium/GrVkSecondaryCBDrawContext.h | 131 + .../chromium/SkChromeRemoteGlyphCache.h | 150 + .../private/chromium/SkDiscardableMemory.h | 70 + .../private/chromium/SkImageChromium.h | 117 + .../skia/include/private/chromium/Slug.h | 72 + .../private/gpu/ganesh/GrContext_Base.h | 104 + .../private/gpu/ganesh/GrD3DTypesMinimal.h | 74 + .../private/gpu/ganesh/GrImageContext.h | 56 + .../private/gpu/ganesh/GrTextureGenerator.h | 66 + .../include/private/gpu/ganesh/GrTypesPriv.h | 1007 ++++++ .../private/gpu/graphite/ContextOptionsPriv.h | 70 + .../private/gpu/graphite/DawnTypesPriv.h | 61 + .../gpu/graphite/MtlGraphiteTypesPriv.h | 95 + .../gpu/graphite/VulkanGraphiteTypesPriv.h | 83 + .../skia/include/private/gpu/vk/SkiaVulkan.h | 36 + .../PublicHeaders/skia/include/sksl/OWNERS | 3 + .../skia/include/sksl/SkSLDebugTrace.h | 28 + .../skia/include/sksl/SkSLVersion.h | 27 + .../skia/include/svg/SkSVGCanvas.h | 42 + .../skia/include/utils/SkCamera.h | 109 + .../skia/include/utils/SkCanvasStateUtils.h | 81 + .../skia/include/utils/SkCustomTypeface.h | 69 + .../skia/include/utils/SkEventTracer.h | 90 + .../skia/include/utils/SkNWayCanvas.h | 123 + .../skia/include/utils/SkNoDrawCanvas.h | 78 + .../skia/include/utils/SkNullCanvas.h | 22 + .../skia/include/utils/SkOrderedFontMgr.h | 66 + .../skia/include/utils/SkPaintFilterCanvas.h | 140 + .../skia/include/utils/SkParse.h | 37 + .../skia/include/utils/SkParsePath.h | 25 + .../skia/include/utils/SkShadowUtils.h | 102 + .../skia/include/utils/SkTextUtils.h | 43 + .../skia/include/utils/SkTraceEventPhase.h | 19 + .../skia/include/utils/mac/SkCGUtils.h | 90 + .../PublicHeaders/skia/modules/skcms/BUILD.gn | 100 + .../PublicHeaders/skia/modules/skcms/OWNERS | 2 + .../skia/modules/skcms/README.chromium | 6 + .../PublicHeaders/skia/modules/skcms/skcms.cc | 2889 +++++++++++++++++ .../skia/modules/skcms/skcms.gni | 57 + .../PublicHeaders/skia/modules/skcms/skcms.h | 10 + .../skia/modules/skcms/src/Transform_inl.h | 1541 +++++++++ .../skia/modules/skcms/src/skcms_Transform.h | 162 + .../skcms/src/skcms_TransformBaseline.cc | 48 + .../modules/skcms/src/skcms_TransformHsw.cc | 61 + .../modules/skcms/src/skcms_TransformSkx.cc | 58 + .../skia/modules/skcms/src/skcms_internals.h | 138 + .../skia/modules/skcms/src/skcms_public.h | 406 +++ .../skia/modules/skcms/version.sha1 | 1 + .../skia/device/libskia.framework/Info.plist | Bin 0 -> 756 bytes .../skia/device/libskia.framework/libskia | Bin 0 -> 2954736 bytes .../simulator/libskia.framework/Info.plist | Bin 0 -> 756 bytes .../skia/simulator/libskia.framework/libskia | Bin 0 -> 2961400 bytes build-system/bazel-rules/rules_xcodeproj | 2 +- .../Sources/AccountContext.swift | 33 +- .../Sources/ChatController.swift | 16 +- .../Sources/GalleryController.swift | 2 +- .../Sources/OpenChatMessage.swift | 3 + .../AccountContext/Sources/Premium.swift | 3 + .../Sources/PresentationCallManager.swift | 2 + ...AttachmentTextInputActionButtonsNode.swift | 4 +- submodules/AttachmentUI/BUILD | 1 + .../Sources/AttachmentContainer.swift | 101 +- .../Sources/AttachmentController.swift | 158 +- .../Sources/AttachmentPanel.swift | 37 +- submodules/AudioBlob/BUILD | 8 +- .../AuthorizationSequenceController.swift | 2 +- .../AvatarNode/Sources/AvatarNode.swift | 4 +- .../Sources/BotCheckoutController.swift | 2 +- submodules/BrowserUI/BUILD | 1 + .../BrowserUI/Sources/BrowserContent.swift | 6 +- .../Sources/BrowserInstantPageContent.swift | 2 +- .../BrowserNavigationBarComponent.swift | 12 +- .../BrowserUI/Sources/BrowserScreen.swift | 32 +- .../Sources/BrowserSearchBarComponent.swift | 6 +- .../Sources/BrowserStackContainerNode.swift | 445 --- .../Sources/BrowserToolbarComponent.swift | 12 +- .../BrowserUI/Sources/BrowserWebContent.swift | 6 +- .../Sources/CalendarMessageScreen.swift | 22 +- submodules/Camera/Sources/CameraMetrics.swift | 4 +- .../Sources/ChatInterfaceState.swift | 54 +- submodules/ChatListUI/BUILD | 1 + .../Sources/ChatListContainerItemNode.swift | 4 +- .../Sources/ChatListController.swift | 254 +- .../Sources/ChatListControllerNode.swift | 14 +- .../Sources/ChatListEmptyNode.swift | 2 +- .../ChatListFilterPresetController.swift | 2 +- .../Sources/ChatListSearchContainerNode.swift | 2 +- .../Sources/ChatListSearchListPaneNode.swift | 134 +- .../ChatListSearchPaneContainerNode.swift | 6 +- .../Sources/NicegramButtonComponent.swift | 2 +- .../Sources/Node/ChatListItem.swift | 38 +- .../Sources/Node/ChatListItemStrings.swift | 85 +- .../ChatPresentationInterfaceState.swift | 10 +- .../Sources/ChatTextFormat.swift | 24 + ...ChatSendMessageActionSheetController.swift | 5 + .../ChatSendMessageContextScreen.swift | 94 +- .../Sources/MessageItemView.swift | 48 +- .../Sources/SendButton.swift | 36 +- .../Sources/ChatTextLinkEditController.swift | 20 +- .../Base/ChildComponentTransitions.swift | 32 +- .../Source/Base/CombinedComponent.swift | 92 +- .../ComponentFlow/Source/Base/Component.swift | 12 +- .../Source/Base/Transition.swift | 26 +- .../Source/Components/Button.swift | 6 +- .../Source/Components/Circle.swift | 4 +- .../Source/Components/Image.swift | 4 +- .../Source/Components/List.swift | 4 +- .../Source/Components/Rectangle.swift | 2 +- .../Source/Components/RoundedRectangle.swift | 4 +- .../Source/Components/Text.swift | 2 +- .../Source/Host/ComponentHostView.swift | 10 +- .../Sources/ActivityIndicatorComponent.swift | 4 +- .../Sources/AnimatedStickerComponent.swift | 4 +- .../Sources/BalancedTextComponent.swift | 4 +- .../Sources/BlurredBackgroundComponent.swift | 4 +- .../Sources/BundleIconComponent.swift | 4 +- .../Sources/ComponentDisplayAdapters.swift | 6 +- .../Sources/CreditCardInputComponent.swift | 4 +- .../Sources/PrefixSectionGroupComponent.swift | 4 +- .../Sources/TextInputComponent.swift | 4 +- .../Sources/LottieAnimationComponent.swift | 4 +- .../Sources/MultilineTextComponent.swift | 4 +- .../MultilineTextWithEntitiesComponent.swift | 4 +- .../Sources/PagerComponent.swift | 56 +- .../Sources/ProgressIndicatorComponent.swift | 4 +- .../Sources/ReactionButtonListComponent.swift | 161 +- .../ReactionListContextMenuContent.swift | 53 +- .../Sources/SheetComponent.swift | 11 +- .../Sources/SolidRoundedButtonComponent.swift | 4 +- .../Sources/UndoPanelComponent.swift | 4 +- .../Sources/UndoPanelContainerComponent.swift | 6 +- .../Sources/ViewControllerComponent.swift | 10 +- .../Sources/ComposePollScreen.swift | 14 +- .../ListComposePollOptionComponent.swift | 20 +- .../Sources/ContactListNode.swift | 71 +- .../Sources/ContactsControllerNode.swift | 8 +- .../Sources/ContactsPeerItem.swift | 2 +- submodules/ContextUI/BUILD | 1 + .../Sources/ContextActionsContainerNode.swift | 6 + .../ContextUI/Sources/ContextController.swift | 11 + .../ContextControllerActionsStackNode.swift | 35 +- ...tControllerExtractedPresentationNode.swift | 87 + .../Sources/ContextSourceContainer.swift | 4 +- .../Sources/ReactionPreviewView.swift | 55 + ...onSequenceCountrySelectionController.swift | 2 +- .../Sources/DebugController.swift | 27 +- .../Sources/DeviceLocationManager.swift | 2 +- .../ContainedViewLayoutTransition.swift | 43 +- .../Display/Source/ContextMenuAction.swift | 1 + submodules/Display/Source/KeyShortcut.swift | 8 +- .../Navigation/MinimizedContainer.swift | 18 + .../Navigation/NavigationContainer.swift | 9 + .../Navigation/NavigationController.swift | 133 +- .../Source/Navigation/NavigationLayout.swift | 1 + .../Navigation/NavigationModalContainer.swift | 3 + submodules/Display/Source/NavigationBar.swift | 8 +- .../Display/Source/ViewController.swift | 21 + .../DrawingUI/Sources/ColorPickerScreen.swift | 18 +- .../Sources/DrawingEntitiesView.swift | 21 +- .../Sources/DrawingLinkEntityView.swift | 624 ++++ .../Sources/DrawingLocationEntityView.swift | 6 +- .../Sources/DrawingReactionView.swift | 2 +- .../DrawingUI/Sources/DrawingScreen.swift | 63 +- .../DrawingSimpleShapeEntityView.swift | 6 +- .../Sources/DrawingStickerEntityView.swift | 10 +- .../Sources/DrawingTextEntityView.swift | 10 +- .../Sources/DrawingVectorEntityView.swift | 6 +- .../DrawingUI/Sources/DrawingView.swift | 2 +- .../Sources/ModeAndSizeComponent.swift | 4 +- .../Sources/TextSettingsComponent.swift | 14 +- .../DrawingUI/Sources/ToolsComponent.swift | 6 +- .../Sources/FileMediaResourceStatus.swift | 4 +- .../GalleryData/Sources/GalleryData.swift | 11 +- .../ChatItemGalleryFooterContentNode.swift | 13 +- .../GalleryUI/Sources/GalleryController.swift | 541 +-- .../Sources/GalleryControllerNode.swift | 6 +- .../GalleryUI/Sources/GalleryFooterNode.swift | 2 + .../Sources/Items/ChatImageGalleryItem.swift | 42 +- .../Items/UniversalVideoGalleryItem.swift | 77 +- .../SecretMediaPreviewController.swift | 3 +- submodules/Geocoding/Sources/Geocoding.swift | 7 +- .../Controllers/BaseChartController.swift | 12 + .../Lines/GeneralLinesChartController.swift | 2 +- .../BarsComponentController.swift | 2 +- .../StackedBarsChartController.swift | 28 +- .../Renderes/VerticalScalesRenderer.swift | 47 +- .../Helpers/ScalesNumberFormatter.swift | 6 +- .../Sources/Helpers/UIColor+Utils.swift | 2 +- .../Sources/Helpers/UIView+Extensions.swift | 2 +- submodules/GraphUI/Sources/ChartNode.swift | 38 +- .../GraphUI/Sources/ChartVisibilityView.swift | 2 +- submodules/HashtagSearchUI/BUILD | 4 + .../Sources/HashtagSearchController.swift | 23 +- .../Sources/HashtagSearchControllerNode.swift | 171 +- .../HashtagSearchGlobalChatContents.swift | 4 +- .../HashtagSearchNavigationContentNode.swift | 14 +- .../Sources/StoryResultsPanelComponent.swift | 193 ++ .../Sources/InstantPageDetailsNode.swift | 2 +- .../Sources/InstantPageLayout.swift | 2 +- .../Sources/InvisibleInkDustNode.swift | 63 +- ...ItemListControllerSegmentedTitleView.swift | 2 +- .../ItemListControllerTabsContentNode.swift | 2 +- submodules/JoinLinkPreviewUI/BUILD | 2 +- .../Sources/JoinLinkPreviewController.swift | 2 +- .../JoinLinkPreviewPeerContentNode.swift | 2 +- .../PublicHeaders/LegacyComponents/PGCamera.h | 2 + .../LegacyComponents/PGCameraCaptureSession.h | 3 + .../PGCameraVolumeButtonHandler.h | 3 +- .../LegacyComponents/TGCameraMainView.h | 3 +- .../LegacyComponents/TGMediaEditingContext.h | 6 + .../TGMediaSelectionContext.h | 2 +- .../LegacyComponents/Sources/PGCamera.m | 8 + .../Sources/PGCameraCaptureSession.m | 8 + .../Sources/PGCameraVolumeButtonHandler.m | 57 +- .../Sources/TGCameraController.m | 52 +- .../Sources/TGCameraMainPhoneView.m | 2 +- .../Sources/TGCameraMainTabletView.m | 2 +- .../Sources/TGMediaAssetsController.m | 22 + .../Sources/TGMediaEditingContext.m | 104 +- .../Sources/TGVideoMessageCaptureController.m | 2 +- .../Sources/LegacyMediaPickers.swift | 209 +- .../Sources/LegacyPaintStickersContext.swift | 17 +- .../Sources/ListMessageFileItemNode.swift | 2 +- .../Sources/LocationAnnotation.swift | 46 +- .../Sources/LocationInfoListItem.swift | 16 +- .../Sources/LocationMapHeaderNode.swift | 42 +- .../LocationUI/Sources/LocationMapNode.swift | 171 +- .../Sources/LocationOptionsNode.swift | 15 +- .../Sources/LocationPickerController.swift | 31 +- .../LocationPickerControllerNode.swift | 154 +- .../Sources/LocationSearchContainerNode.swift | 2 +- .../LocationUI/Sources/LocationUtils.swift | 24 +- .../Sources/LocationViewControllerNode.swift | 301 +- submodules/LottieCpp/BUILD | 56 + submodules/LottieCpp/lottiecpp | 1 + .../Sources/MediaPickerGridItem.swift | 80 +- .../Sources/MediaPickerScreen.swift | 100 +- .../Sources/MediaPickerSelectedListNode.swift | 180 +- submodules/MediaPlayer/Package.swift | 8 +- .../Sources/PaymentCardEntryScreen.swift | 4 +- .../Sources/PeerInfoAvatarListNode.swift | 6 +- submodules/PeerInfoUI/BUILD | 2 + .../Sources/ChannelAdminController.swift | 2 + .../ChannelBannedMemberController.swift | 1 + ...hannelDiscussionGroupSetupController.swift | 1 + .../ChannelPermissionsController.swift | 1 + .../Sources/ChannelVisibilityController.swift | 6 +- .../ConvertToSupergroupController.swift | 2 +- .../GroupPreHistorySetupController.swift | 1 + .../Sources/PeersNearbyController.swift | 2 +- .../Postbox/Sources/ChatListViewState.swift | 1 + submodules/Postbox/Sources/Message.swift | 4 + .../Postbox/Sources/MessageHistoryTable.swift | 22 +- .../Postbox/Sources/MessageHistoryView.swift | 15 +- .../Sources/MessageHistoryViewState.swift | 168 +- .../Sources/MessageOfInterestHolesView.swift | 6 +- submodules/Postbox/Sources/Postbox.swift | 35 +- submodules/Postbox/Sources/Views.swift | 4 + .../Sources/AppIconsDemoComponent.swift | 4 +- .../PremiumUI/Sources/BadgeLabelView.swift | 4 +- .../BoostHeaderBackgroundComponent.swift | 4 +- .../Sources/BusinessPageComponent.swift | 4 +- .../Sources/CreateGiveawayFooterItem.swift | 2 +- .../Sources/EmojiHeaderComponent.swift | 4 +- .../Sources/IncreaseLimitFooterItem.swift | 8 +- .../Sources/IncreaseLimitHeaderItem.swift | 15 +- .../Sources/PageIndicatorComponent.swift | 4 +- .../Sources/PhoneDemoComponent.swift | 4 +- .../Sources/PremiumBoostLevelsScreen.swift | 32 +- .../Sources/PremiumCoinComponent.swift | 4 +- .../PremiumUI/Sources/PremiumDemoScreen.swift | 35 +- .../Sources/PremiumGiftCodeScreen.swift | 8 +- .../PremiumUI/Sources/PremiumGiftScreen.swift | 7 +- .../Sources/PremiumIntroScreen.swift | 50 +- .../Sources/PremiumLimitScreen.swift | 22 +- .../Sources/PremiumLimitsListScreen.swift | 39 +- .../Sources/PremiumOptionComponent.swift | 4 +- .../Sources/ReplaceBoostScreen.swift | 24 +- .../Sources/StickersCarouselComponent.swift | 4 +- .../Sources/StoriesPageComponent.swift | 4 +- .../Sources/ReactionContextNode.swift | 15 +- .../Sources/ReactionSelectionNode.swift | 32 + .../Source/Signal_Combine.swift | 6 + .../Sources/ScreenCaptureDetection.swift | 2 +- .../Search/SettingsSearchableItems.swift | 4 +- .../Sources/ShareControllerNode.swift | 8 +- .../ShareItems/Sources/ShareItems.swift | 4 +- .../Sources/SparseItemGrid.swift | 23 + .../Sources/SparseItemGridScrollingArea.swift | 30 +- submodules/StatisticsUI/BUILD | 9 +- .../Sources/BoostHeaderItem.swift | 2 +- .../StatisticsUI/Sources/BoostsTabsItem.swift | 4 +- .../Sources/ChannelStatsController.swift | 542 +++- .../Sources/MonetizationBalanceItem.swift | 198 +- .../Sources/MonetizationUtils.swift | 10 +- .../Sources/RevenueWithdrawalController.swift | 2 +- .../Sources/StarsTransactionItem.swift | 397 +++ .../StatisticsUI/Sources/StatsGraphItem.swift | 54 +- .../Sources/StatsMessageItem.swift | 2 +- .../Sources/StatsOverviewItem.swift | 250 +- submodules/TelegramApi/Sources/Api0.swift | 50 +- submodules/TelegramApi/Sources/Api1.swift | 76 +- submodules/TelegramApi/Sources/Api10.swift | 30 + submodules/TelegramApi/Sources/Api12.swift | 156 +- submodules/TelegramApi/Sources/Api13.swift | 224 +- submodules/TelegramApi/Sources/Api14.swift | 192 +- submodules/TelegramApi/Sources/Api15.swift | 30 + submodules/TelegramApi/Sources/Api2.swift | 90 +- submodules/TelegramApi/Sources/Api23.swift | 140 +- submodules/TelegramApi/Sources/Api25.swift | 96 +- submodules/TelegramApi/Sources/Api27.swift | 30 +- submodules/TelegramApi/Sources/Api28.swift | 36 +- submodules/TelegramApi/Sources/Api3.swift | 50 - submodules/TelegramApi/Sources/Api33.swift | 260 +- submodules/TelegramApi/Sources/Api34.swift | 556 ++-- submodules/TelegramApi/Sources/Api35.swift | 384 +++ submodules/TelegramApi/Sources/Api36.swift | 128 +- submodules/TelegramApi/Sources/Api6.swift | 18 +- submodules/TelegramApi/Sources/Api7.swift | 212 +- submodules/TelegramApi/Sources/Api8.swift | 212 +- submodules/TelegramApi/Sources/Api9.swift | 96 + submodules/TelegramCallsUI/BUILD | 1 + .../Sources/CallControllerNodeV2.swift | 4 +- .../Sources/CallStatusBarNode.swift | 1 + .../Components/AnimatedCounterView.swift | 2 +- .../Components/MediaStreamComponent.swift | 12 +- .../MediaStreamVideoComponent.swift | 47 +- .../Components/ParticipantsComponent.swift | 2 +- .../Components/StreamSheetComponent.swift | 6 +- .../Sources/GroupVideoNode.swift | 3 + .../Sources/MetalVideoRenderingView.swift | 3 + .../Sources/PresentationGroupCall.swift | 16 +- .../SampleBufferVideoRenderingView.swift | 3 + .../Sources/VideoRenderingContext.swift | 225 +- .../Sources/VoiceChatActionButton.swift | 1760 ---------- .../Sources/VoiceChatController.swift | 121 +- .../VoiceChatFullscreenParticipantItem.swift | 1 + .../Sources/VoiceChatOverlayController.swift | 1 + .../Sources/VoiceChatParticipantItem.swift | 1 + .../VoiceChatRecordingSetupController.swift | 1 + .../Account/AccountIntermediateState.swift | 21 +- .../Sources/Account/AccountManager.swift | 2 + .../ApiUtils/ReactionsMessageAttribute.swift | 23 +- .../ApiUtils/StoreMessage_Telegram.swift | 93 +- .../ApiUtils/TelegramExtendedMedia.swift | 27 + .../Sources/ApiUtils/TelegramMediaMap.swift | 12 +- .../TelegramCore/Sources/Authorization.swift | 8 +- .../Sources/Network/FetchV2.swift | 410 ++- .../Network/FetchedMediaResource.swift | 8 +- .../Sources/Network/Network.swift | 11 +- .../PendingMessageUploadedContent.swift | 165 +- .../PendingMessages/RequestEditMessage.swift | 28 +- .../StandaloneSendMessage.swift | 2 +- .../Sources/State/AccountState.swift | 2 +- .../State/AccountStateManagementUtils.swift | 58 +- .../Sources/State/AccountStateManager.swift | 41 + .../Sources/State/AccountViewTracker.swift | 22 +- .../Sources/State/ApplyUpdateMessage.swift | 6 + ...dSynchronizeChatInputStateOperations.swift | 2 +- .../Sources/State/PendingMessageManager.swift | 97 +- ...ecretChatIncomingDecryptedOperations.swift | 16 +- .../Sources/State/Serialization.swift | 2 +- .../State/UserLimitsConfiguration.swift | 5 + .../Statistics/StarsRevenueStatistics.swift | 350 ++ .../SyncCore/SyncCore_CachedChannelData.swift | 1 + .../SyncCore/SyncCore_NetworkSettings.swift | 4 +- .../SyncCore_ReactionsMessageAttribute.swift | 6 + ...ncCore_SynchronizeableChatInputState.swift | 9 +- .../SyncCore/SyncCore_TelegramMediaMap.swift | 45 +- .../SyncCore_TelegramMediaPaidContent.swift | 59 + .../ChangeAccountPhoneNumber.swift | 4 +- .../Data/ConfigurationData.swift | 4 + .../TelegramEngine/Data/PeersData.swift | 28 + .../TelegramEngine/Messages/BotWebView.swift | 81 +- .../Messages/ClearCloudDrafts.swift | 2 +- .../Messages/ExtendedMedia.swift | 40 + .../TelegramEngine/Messages/Media.swift | 7 + .../TelegramEngine/Messages/MediaArea.swift | 39 +- .../Messages/QuickReplyMessages.swift | 2 +- .../Messages/ReplyThreadHistory.swift | 1 + .../Messages/SparseMessageList.swift | 2 +- .../TelegramEngine/Messages/Stories.swift | 11 +- .../Messages/StoryListContext.swift | 604 +++- .../Messages/TelegramEngineMessages.swift | 12 +- .../Payments/BotPaymentForm.swift | 23 + .../TelegramEngine/Payments/Stars.swift | 205 +- .../Payments/TelegramEnginePayments.swift | 13 +- .../Peers/InactiveChannels.swift | 2 +- .../Peers/TelegramEnginePeers.swift | 18 +- .../Peers/UpdateCachedPeerData.swift | 3 + .../Sources/Utils/MessageUtils.swift | 4 + .../Sources/ComponentsThemes.swift | 2 +- .../DefaultDarkPresentationTheme.swift | 24 + .../DefaultDarkTintedPresentationTheme.swift | 24 + .../Sources/DefaultDayPresentationTheme.swift | 54 +- .../Sources/PresentationData.swift | 4 +- .../Sources/PresentationTheme.swift | 20 + .../Sources/PresentationThemeCodable.swift | 4 + .../PresentationResourcesSettings.swift | 18 + .../Sources/MessageContentKind.swift | 20 + .../Sources/ServiceMessageStrings.swift | 2 +- submodules/TelegramUI/BUILD | 5 + .../Sources/ActionPanelComponent.swift | 4 +- .../AdminUserActionsPeerComponent.swift | 4 +- .../Sources/AdminUserActionsSheet.swift | 20 +- .../Sources/RecentActionsSettingsSheet.swift | 14 +- .../Sources/AdsReportScreen.swift | 14 +- .../Sources/AnimatedCounterComponent.swift | 10 +- .../Sources/AnimatedTextComponent.swift | 4 +- .../AudioTranscriptionButtonComponent.swift | 4 +- ...anscriptionPendingIndicatorComponent.swift | 8 +- .../Sources/AudioWaveformComponent.swift | 4 +- .../Sources/AvatarEditorScreen.swift | 10 +- .../Sources/AvatarPreviewComponent.swift | 4 +- .../Sources/BackgroundColorComponent.swift | 8 +- .../Sources/ColorPickerComponent.swift | 4 +- .../Sources/BackButtonComponent.swift | 4 +- .../Sources/BottomButtonPanelComponent.swift | 4 +- .../Sources/ButtonComponent.swift | 12 +- .../CallScreen/Metal/CallScreenShaders.metal | 6 +- .../Sources/Components/AvatarLayer.swift | 2 +- .../Sources/Components/ButtonGroupView.swift | 4 +- .../Components/CallBackgroundLayer.swift | 2 +- .../Sources/Components/CallBlobsLayer.swift | 34 +- .../Sources/Components/CloseButtonView.swift | 8 +- .../Components/ContentOverlayButton.swift | 8 +- .../Components/EmojiExpandedInfoView.swift | 8 +- .../Sources/Components/KeyEmojiView.swift | 10 +- .../Sources/Components/NoticeView.swift | 2 +- .../PrivateCallPictureInPictureView.swift | 6 +- .../Components/PrivateCallVideoLayer.swift | 18 +- .../Sources/Components/RatingView.swift | 2 +- .../Components/RoundedCornersView.swift | 2 +- .../Sources/Components/StatusView.swift | 4 +- .../Sources/Components/TitleView.swift | 2 +- .../Components/VideoContainerView.swift | 16 +- .../Sources/Components/VideoShadowsView.swift | 2 +- .../Sources/PrivateCallScreen.swift | 28 +- .../Calls/VoiceChatActionButton/BUILD | 77 + .../Metal/VoiceChatActionButtonShaders.metal | 22 + .../Sources/BlobView.swift | 168 + .../Sources/VoiceBlobView.swift | 174 + .../Sources/VoiceChatActionButton.swift | 521 +++ .../VoiceChatActionButtonBackgroundNode.swift | 744 +++++ .../VoiceChatActionButtonIconNode.swift | 143 + .../Sources/VoiceChatRaiseHandNode.swift | 53 + .../Contents.json | 6 + .../VoiceChatActionButton/Contents.json | 9 + .../Sources/CameraButtonComponent.swift | 6 +- .../CameraScreen/Sources/CameraScreen.swift | 16 +- .../Sources/CaptureControlsComponent.swift | 46 +- .../Sources/FlashTintControlComponent.swift | 10 +- .../CameraScreen/Sources/ModeComponent.swift | 12 +- .../Sources/PlaceholderComponent.swift | 4 +- .../Sources/ShutterBlobView.swift | 16 +- .../CameraScreen/Sources/ZoomComponent.swift | 4 +- .../Sources/ChatAvatarNavigationNode.swift | 2 +- .../Sources/ChatBotInfoItem.swift | 10 +- .../ChatChannelSubscriberInputPanelNode/BUILD | 2 +- .../ChatChannelSubscriberInputPanelNode.swift | 2 +- .../ChatEmptyNode/Sources/ChatEmptyNode.swift | 8 +- .../ChatHistorySearchContainerNode.swift | 2 +- ...ChatInlineSearchResultsListComponent.swift | 16 +- .../Chat/ChatMessageActionButtonsNode/BUILD | 1 + .../ChatMessageActionButtonsNode.swift | 7 +- .../ChatMessageAnimatedStickerItemNode.swift | 14 +- .../ChatMessageAttachedContentNode.swift | 17 +- .../ChatMessageBubbleContentNode.swift | 8 +- .../Chat/ChatMessageBubbleItemNode/BUILD | 2 + .../Sources/ChatMessageBubbleItemNode.swift | 338 +- .../ChatMessageContactBubbleContentNode.swift | 2 +- .../ChatMessageDateAndStatusNode.swift | 10 +- ...hatMessageFactCheckBubbleContentNode.swift | 2 +- ...ChatMessageGiveawayBubbleContentNode.swift | 2 +- .../ChatMessageInstantVideoItemNode.swift | 2 +- .../ChatMessageInteractiveFileNode.swift | 4 +- ...atMessageInteractiveInstantVideoNode.swift | 4 +- .../ChatMessageInteractiveMediaNode/BUILD | 1 + .../ChatMessageInteractiveMediaNode.swift | 246 +- .../Sources/ChatMessageItem.swift | 8 +- .../Sources/ChatMessageItemCommon.swift | 2 +- .../Sources/ChatMessageItemView.swift | 16 +- ...essageJoinedChannelBubbleContentNode.swift | 14 +- .../ChatMessageMapBubbleContentNode.swift | 2 +- .../ChatMessageMediaBubbleContentNode.swift | 83 +- .../ChatMessagePollBubbleContentNode.swift | 2 +- ...hatMessageReactionsFooterContentNode.swift | 17 +- ...atMessageRestrictedBubbleContentNode.swift | 2 +- .../Chat/ChatMessageStarsMediaInfoNode/BUILD | 32 + .../ChatMessageStarsMediaInfoNode.swift | 292 ++ .../Sources/ChatMessageStickerItemNode.swift | 2 +- .../ChatMessageTextBubbleContentNode.swift | 214 +- .../Sources/ChatMessageTransitionNode.swift | 2 +- .../Chat/ChatMessageUnlockMediaNode/BUILD | 37 + .../Sources/ChatMessageUnlockMediaNode.swift | 345 ++ .../ChatMessageWebpageBubbleContentNode.swift | 25 - .../Sources/ChatOverscrollControl.swift | 28 +- .../ChatRecentActionsControllerNode.swift | 12 +- .../ChatRecentActionsHistoryTransition.swift | 2 +- .../ChatSendAudioMessageContextPreview.swift | 32 +- .../Components/Chat/ChatSendStarsScreen/BUILD | 38 + .../Sources/BadgeLabelView.swift | 166 + .../Sources/ChatSendStarsScreen.swift | 1439 ++++++++ .../Sources/TopMessageReactions.swift | 37 +- .../Sources/ChatControllerInteraction.swift | 27 +- .../Sources/ChatEntityKeyboardInputNode.swift | 16 +- .../Sources/ActionListItemComponent.swift | 4 +- .../ChatFolderLinkHeaderComponent.swift | 8 +- .../Sources/ChatFolderLinkPreviewScreen.swift | 18 +- .../Sources/LinkListItemComponent.swift | 10 +- .../Sources/PeerListItemComponent.swift | 4 +- .../Sources/ChatListHeaderComponent.swift | 28 +- .../Sources/ChatListNavigationBar.swift | 8 +- .../Sources/ChatListTitleView.swift | 6 +- .../ChatTitleView/Sources/ChatTitleView.swift | 4 +- .../Sources/ContextMenuActionNode.swift | 115 +- .../Sources/ContextMenuNode.swift | 15 +- .../ContextReferenceButtonComponent.swift | 4 +- .../Sources/DynamicCornerRadiusView.swift | 6 +- .../Sources/EmojiActionIconComponent.swift | 4 +- .../Sources/EmojiStatusComponent.swift | 4 +- .../Sources/EmojiStatusPreviewScreen.swift | 22 +- .../EmojiStatusSelectionComponent.swift | 18 +- .../Sources/EmojiSuggestionsComponent.swift | 4 +- .../Sources/EmojiTextAttachmentView.swift | 65 +- .../EmptyStateIndicatorComponent.swift | 4 +- .../Sources/EmojiKeyboardItemLayer.swift | 2 +- .../Sources/EmojiPagerContentComponent.swift | 40 +- .../Sources/EmojiSearchContent.swift | 8 +- .../Sources/EmojiSearchHeaderView.swift | 6 +- .../EmojiSearchSearchBarComponent.swift | 18 +- .../Sources/EmojiSearchStatusComponent.swift | 4 +- .../Sources/EmptySearchResultsView.swift | 2 +- .../Sources/EntityKeyboard.swift | 48 +- .../EntityKeyboardBottomPanelComponent.swift | 10 +- ...tyKeyboardTopContainerPanelComponent.swift | 18 +- .../EntityKeyboardTopPanelComponent.swift | 42 +- .../EntitySearchContentComponent.swift | 4 +- .../Sources/GifPagerContentComponent.swift | 14 +- .../Sources/GroupEmbeddedView.swift | 2 +- .../Sources/PremiumBadgeView.swift | 2 +- .../EntityKeyboard/Sources/WarpView.swift | 4 +- .../Sources/ForumCreateTopicScreen.swift | 8 +- .../Sources/InteractiveTextComponent.swift | 666 +++- .../InteractiveTextNodeWithEntities.swift | 102 +- .../Sources/LegacyMessageInputPanel.swift | 4 +- .../Sources/ListActionItemComponent.swift | 12 +- .../Sources/ListItemComponentAdaptor.swift | 6 +- .../ListItemSliderSelectorComponent.swift | 4 +- .../ListItemSwipeOptionContainer.swift | 12 +- .../ListMultilineTextFieldItemComponent.swift | 10 +- .../Sources/ListSectionComponent.swift | 20 +- .../Sources/ListTextFieldItemComponent.swift | 4 +- .../Sources/LottieComponent.swift | 4 +- .../TelegramUI/Components/LottieCpp/BUILD | 48 - .../PublicHeaders/LottieCpp/BezierPath.h | 155 - .../PublicHeaders/LottieCpp/CGPath.h | 80 - .../PublicHeaders/LottieCpp/CGPathCocoa.h | 47 - .../LottieCpp/PublicHeaders/LottieCpp/Color.h | 55 - .../PublicHeaders/LottieCpp/CurveVertex.h | 200 -- .../PublicHeaders/LottieCpp/LottieAnimation.h | 28 - .../LottieCpp/LottieAnimationContainer.h | 71 - .../PublicHeaders/LottieCpp/LottieCpp.h | 20 - .../LottieCpp/LottieRenderTree.h | 147 - .../PublicHeaders/LottieCpp/PathElement.h | 94 - .../PublicHeaders/LottieCpp/RenderTreeNode.h | 471 --- .../PublicHeaders/LottieCpp/ShapeAttributes.h | 57 - .../PublicHeaders/LottieCpp/Vectors.h | 278 -- .../PublicHeaders/LottieCpp/VectorsCocoa.h | 17 - .../PublicHeaders/LottieCpp/lottiejson11.hpp | 236 -- .../CompLayers/CompositionLayer.cpp | 17 - .../CompLayers/CompositionLayer.hpp | 201 -- .../CompLayers/ImageCompositionLayer.cpp | 5 - .../CompLayers/ImageCompositionLayer.hpp | 43 - .../CompLayers/MaskContainerLayer.cpp | 5 - .../CompLayers/MaskContainerLayer.hpp | 178 - .../CompLayers/NullCompositionLayer.cpp | 5 - .../CompLayers/NullCompositionLayer.hpp | 17 - .../CompLayers/PreCompositionLayer.cpp | 5 - .../CompLayers/PreCompositionLayer.hpp | 202 -- .../CompLayers/ShapeCompositionLayer.cpp | 1378 -------- .../CompLayers/ShapeCompositionLayer.hpp | 36 - .../CompLayers/ShapeUtils/BezierPathUtils.cpp | 487 --- .../CompLayers/ShapeUtils/BezierPathUtils.hpp | 39 - .../CompLayers/TextCompositionLayer.cpp | 5 - .../CompLayers/TextCompositionLayer.hpp | 81 - .../MainThreadAnimationLayer.cpp | 5 - .../MainThreadAnimationLayer.hpp | 279 -- .../Utility/CompositionLayersInitializer.cpp | 111 - .../Utility/CompositionLayersInitializer.hpp | 23 - .../Utility/LayerFontProvider.cpp | 5 - .../Utility/LayerFontProvider.hpp | 45 - .../Utility/LayerImageProvider.cpp | 5 - .../Utility/LayerImageProvider.hpp | 58 - .../Utility/LayerTextProvider.cpp | 5 - .../Utility/LayerTextProvider.hpp | 45 - .../Utility/LayerTransformNode.cpp | 5 - .../Utility/LayerTransformNode.hpp | 205 -- .../NodeProperties/NodeProperty.cpp | 5 - .../NodeProperties/NodeProperty.hpp | 54 - .../Protocols/AnyNodeProperty.cpp | 5 - .../Protocols/AnyNodeProperty.hpp | 33 - .../Protocols/AnyValueContainer.cpp | 5 - .../Protocols/AnyValueContainer.hpp | 25 - .../Protocols/HasRenderUpdates.hpp | 13 - .../NodeProperties/Protocols/HasUpdate.hpp | 14 - .../Protocols/KeypathSearchable.cpp | 5 - .../Protocols/KeypathSearchable.hpp | 36 - .../Protocols/NodePropertyMap.cpp | 5 - .../Protocols/NodePropertyMap.hpp | 41 - .../NodeProperties/ValueContainer.cpp | 5 - .../NodeProperties/ValueContainer.hpp | 58 - .../DashPatternInterpolator.cpp | 5 - .../DashPatternInterpolator.hpp | 48 - .../ValueProviders/KeyframeInterpolator.cpp | 5 - .../ValueProviders/KeyframeInterpolator.hpp | 452 --- .../ValueProviders/SingleValueProvider.cpp | 5 - .../ValueProviders/SingleValueProvider.hpp | 42 - .../OutputNodes/PassThroughOutputNode.hpp | 72 - .../Nodes/RenderNodes/StrokeNode.hpp | 36 - .../Nodes/Text/TextAnimatorNode.hpp | 367 --- .../Protocols/AnimatorNode.hpp | 238 -- .../NodeRenderSystem/Protocols/NodeOutput.hpp | 28 - .../NodeRenderSystem/Protocols/RenderNode.hpp | 73 - .../RenderLayers/GetGradientParameters.cpp | 99 - .../RenderLayers/GetGradientParameters.hpp | 13 - .../Lottie/Private/Model/Animation.cpp | 5 - .../Lottie/Private/Model/Animation.hpp | 314 -- .../Lottie/Private/Model/Assets/Asset.cpp | 5 - .../Lottie/Private/Model/Assets/Asset.hpp | 50 - .../Private/Model/Assets/AssetLibrary.cpp | 5 - .../Private/Model/Assets/AssetLibrary.hpp | 71 - .../Private/Model/Assets/ImageAsset.cpp | 5 - .../Private/Model/Assets/ImageAsset.hpp | 125 - .../Private/Model/Assets/PrecompAsset.cpp | 5 - .../Private/Model/Assets/PrecompAsset.hpp | 66 - .../Private/Model/Keyframes/KeyframeGroup.cpp | 5 - .../Private/Model/Keyframes/KeyframeGroup.hpp | 150 - .../Private/Model/Layers/ImageLayerModel.cpp | 5 - .../Private/Model/Layers/ImageLayerModel.hpp | 40 - .../Private/Model/Layers/LayerModel.cpp | 45 - .../Private/Model/Layers/LayerModel.hpp | 318 -- .../Model/Layers/LayerModelSerialization.cpp | 34 - .../Model/Layers/LayerModelSerialization.hpp | 14 - .../Model/Layers/PreCompLayerModel.cpp | 5 - .../Model/Layers/PreCompLayerModel.hpp | 57 - .../Private/Model/Layers/ShapeLayerModel.cpp | 5 - .../Private/Model/Layers/ShapeLayerModel.hpp | 45 - .../Private/Model/Layers/SolidLayerModel.cpp | 5 - .../Private/Model/Layers/SolidLayerModel.hpp | 42 - .../Private/Model/Layers/TextLayerModel.cpp | 5 - .../Private/Model/Layers/TextLayerModel.hpp | 83 - .../Private/Model/Objects/DashElement.cpp | 5 - .../Private/Model/Objects/DashElement.hpp | 78 - .../Private/Model/Objects/FitzModifier.cpp | 5 - .../Private/Model/Objects/FitzModifier.hpp | 53 - .../Lottie/Private/Model/Objects/Marker.cpp | 5 - .../Lottie/Private/Model/Objects/Marker.hpp | 52 - .../Lottie/Private/Model/Objects/Mask.cpp | 5 - .../Lottie/Private/Model/Objects/Mask.hpp | 139 - .../Private/Model/Objects/Transform.cpp | 5 - .../Private/Model/Objects/Transform.hpp | 267 -- .../Private/Model/ShapeItems/Ellipse.cpp | 5 - .../Private/Model/ShapeItems/Ellipse.hpp | 64 - .../Lottie/Private/Model/ShapeItems/Fill.cpp | 6 - .../Lottie/Private/Model/ShapeItems/Fill.hpp | 71 - .../Private/Model/ShapeItems/GradientFill.cpp | 5 - .../Private/Model/ShapeItems/GradientFill.hpp | 113 - .../Model/ShapeItems/GradientStroke.cpp | 5 - .../Model/ShapeItems/GradientStroke.hpp | 193 -- .../Lottie/Private/Model/ShapeItems/Group.cpp | 5 - .../Lottie/Private/Model/ShapeItems/Group.hpp | 53 - .../Lottie/Private/Model/ShapeItems/Merge.cpp | 5 - .../Lottie/Private/Model/ShapeItems/Merge.hpp | 64 - .../Private/Model/ShapeItems/Rectangle.cpp | 5 - .../Private/Model/ShapeItems/Rectangle.hpp | 101 - .../Private/Model/ShapeItems/Repeater.cpp | 5 - .../Private/Model/ShapeItems/Repeater.hpp | 100 - .../Model/ShapeItems/RoundedRectangle.cpp | 5 - .../Model/ShapeItems/RoundedRectangle.hpp | 77 - .../Lottie/Private/Model/ShapeItems/Shape.cpp | 5 - .../Lottie/Private/Model/ShapeItems/Shape.hpp | 55 - .../Private/Model/ShapeItems/ShapeItem.cpp | 55 - .../Private/Model/ShapeItems/ShapeItem.hpp | 209 -- .../Model/ShapeItems/ShapeTransform.cpp | 5 - .../Model/ShapeItems/ShapeTransform.hpp | 91 - .../Lottie/Private/Model/ShapeItems/Star.cpp | 5 - .../Lottie/Private/Model/ShapeItems/Star.hpp | 133 - .../Private/Model/ShapeItems/Stroke.cpp | 5 - .../Private/Model/ShapeItems/Stroke.hpp | 142 - .../Lottie/Private/Model/ShapeItems/Trim.cpp | 5 - .../Lottie/Private/Model/ShapeItems/Trim.hpp | 57 - .../Lottie/Private/Model/Text/Font.cpp | 5 - .../Lottie/Private/Model/Text/Font.hpp | 103 - .../Lottie/Private/Model/Text/Glyph.cpp | 5 - .../Lottie/Private/Model/Text/Glyph.hpp | 108 - .../Private/Model/Text/TextAnimator.cpp | 5 - .../Private/Model/Text/TextAnimator.hpp | 183 -- .../Private/Model/Text/TextDocument.cpp | 5 - .../Private/Model/Text/TextDocument.hpp | 190 -- .../Lottie/Private/Parsing/JsonParsing.cpp | 219 -- .../Lottie/Private/Parsing/JsonParsing.hpp | 52 - .../Private/Utility/Primitives/BezierPath.cpp | 690 ---- .../Utility/Primitives/CompoundBezierPath.cpp | 5 - .../Utility/Primitives/CompoundBezierPath.hpp | 163 - .../Utility/Primitives/CoordinateSpace.cpp | 5 - .../Utility/Primitives/CoordinateSpace.hpp | 13 - .../Utility/Primitives/CurveVertex.cpp | 5 - .../Utility/Primitives/PathElement.cpp | 5 - .../DynamicProperties/AnimationKeypath.cpp | 5 - .../DynamicProperties/AnimationKeypath.hpp | 51 - .../DynamicProperties/AnyValueProvider.cpp | 5 - .../DynamicProperties/AnyValueProvider.hpp | 38 - .../FontProvider/AnimationFontProvider.cpp | 5 - .../FontProvider/AnimationFontProvider.hpp | 33 - .../ImageProvider/AnimationImageProvider.hpp | 16 - .../Public/Keyframes/Interpolatable.cpp | 15 - .../Public/Keyframes/Interpolatable.hpp | 14 - .../Lottie/Public/Keyframes/Keyframe.cpp | 5 - .../Lottie/Public/Keyframes/Keyframe.hpp | 259 -- .../Public/Keyframes/ValueInterpolators.cpp | 20 - .../Public/Keyframes/ValueInterpolators.hpp | 227 -- .../Public/Primitives/AnimationTime.cpp | 5 - .../Public/Primitives/AnimationTime.hpp | 14 - .../Lottie/Public/Primitives/AnyValue.cpp | 5 - .../Lottie/Public/Primitives/AnyValue.hpp | 245 -- .../Lottie/Public/Primitives/CALayer.cpp | 37 - .../Lottie/Public/Primitives/CALayer.hpp | 212 -- .../Lottie/Public/Primitives/CGPath.cpp | 193 -- .../Lottie/Public/Primitives/CGPath.mm | 223 -- .../Lottie/Public/Primitives/CTFont.cpp | 5 - .../Lottie/Public/Primitives/CTFont.hpp | 12 - .../Lottie/Public/Primitives/Color.cpp | 128 - .../Lottie/Public/Primitives/DashPattern.cpp | 5 - .../Lottie/Public/Primitives/DashPattern.hpp | 18 - .../Public/Primitives/GradientColorSet.cpp | 5 - .../Public/Primitives/GradientColorSet.hpp | 42 - .../Lottie/Public/Primitives/Vectors.mm | 912 ------ .../TextProvider/AnimationTextProvider.cpp | 5 - .../TextProvider/AnimationTextProvider.hpp | 50 - .../LottieCpp/Sources/LottieAnimation.mm | 60 - .../Sources/LottieAnimationContainer.mm | 85 - .../LottieAnimationContainerInternal.h | 13 - .../Sources/LottieAnimationInternal.h | 15 - .../LottieCpp/Sources/LottieRenderTree.h | 147 - .../LottieCpp/Sources/LottieRenderTree.mm | 426 --- .../Sources/lottiejson11/lottiejson11.cpp | 790 ----- .../TelegramUI/Components/LottieMetal/BUILD | 2 +- .../LottieMetalAnimatedStickerNode.swift | 13 +- .../LottieMetal/Sources/PathFrameState.swift | 3 +- .../Sources/PathRenderFillState.swift | 4 +- .../Sources/PathRenderStrokeState.swift | 4 +- .../Sources/RenderTreeSerialization.swift | 516 --- .../Drawing/CodableDrawingEntity.swift | 55 +- .../Sources/Drawing/DrawingLinkEntity.swift | 260 ++ .../Drawing/DrawingStickerEntity.swift | 10 +- .../Sources/DrawingMessageRenderer.swift | 71 +- .../Sources/MediaEditorComposerEntity.swift | 6 + .../Sources/MediaEditorDraft.swift | 8 +- .../Components/MediaEditorScreen/BUILD | 3 + .../Sources/AdjustmentsComponent.swift | 16 +- .../Sources/BlurComponent.swift | 14 +- .../Sources/CreateLinkOptions.swift | 235 ++ .../Sources/CreateLinkScreen.swift | 1034 ++++++ .../Sources/CurvesComponent.swift | 20 +- .../Sources/FlipButtonContentComponent.swift | 4 +- .../Sources/MediaCutoutScreen.swift | 12 +- .../Sources/MediaEditorScreen.swift | 262 +- .../Sources/MediaToolsScreen.swift | 24 +- .../Sources/SaveProgressScreen.swift | 16 +- .../Sources/StoryPreviewComponent.swift | 8 +- .../Sources/TintComponent.swift | 10 +- .../Sources/MediaScrubberComponent.swift | 26 +- .../MessageInputActionButtonComponent.swift | 8 +- .../Sources/ContextResultPanelComponent.swift | 12 +- .../Sources/MediaPreviewPanelComponent.swift | 6 +- .../MediaRecordingPanelComponent.swift | 8 +- .../Sources/MessageInputPanelComponent.swift | 12 +- .../StickersResultPanelComponent.swift | 12 +- .../Sources/TimeoutContentComponent.swift | 6 +- .../Components/MinimizedContainer/BUILD | 24 + .../Sources/MinimizedContainer.swift | 1004 ++++++ .../Sources/MinimizedHeaderNode.swift | 150 + .../MinimizedContainer/Sources/Utils.swift | 183 ++ .../Sources/NavigationSearchComponent.swift | 4 +- .../Sources/OptionButtonComponent.swift | 6 +- .../Sources/EmojiListInputComponent.swift | 4 +- .../Sources/EmojiSelectionComponent.swift | 6 +- .../Sources/ListSwitchItemComponent.swift | 4 +- .../Sources/PeerAllowedReactionsScreen.swift | 129 +- .../Sources/PeerInfoChatListPaneNode.swift | 6 +- .../Sources/PeerInfoChatPaneNode.swift | 2 +- .../Sources/PeerInfoCoverComponent.swift | 4 +- .../Components/PeerInfo/PeerInfoScreen/BUILD | 1 + ...PeerInfoAvatarTransformContainerNode.swift | 2 +- .../PeerInfoScreen/Sources/PeerInfoData.swift | 60 +- .../Sources/PeerInfoHeaderNode.swift | 14 +- .../Sources/PeerInfoPaneContainerNode.swift | 2 +- .../Sources/PeerInfoScreen.swift | 84 +- .../PeerInfo/PeerInfoStoryGridScreen/BUILD | 2 + .../Sources/PeerInfoStoryGridScreen.swift | 11 +- .../Sources/StorySearchGridScreen.swift | 94 +- .../PeerInfoVisualMediaPaneNode/BUILD | 2 + .../Sources/PeerInfoStoryPaneNode.swift | 833 ++++- .../Sources/PeerInfoVisualMediaPaneNode.swift | 5 +- .../OldChannelsController/BUILD | 34 + .../Sources/OldChannelsController.swift | 4 +- .../Sources/OldChannelsSearch.swift | 14 + .../OwnershipTransferController/BUILD | 30 + .../ChannelOwnershipTransferController.swift | 3 +- .../Sources/OwnershipTransferController.swift | 3 +- .../Sources/PeerSelectionControllerNode.swift | 108 +- .../Sources/PlainButtonComponent.swift | 8 +- .../Sources/GiftAvatarComponent.swift | 8 +- .../Sources/PremiumStarComponent.swift | 4 +- .../PremiumPeerShortcutComponent.swift | 4 +- .../Sources/SavedMessagesScreen.swift | 4 +- .../Sources/ScrollComponent.swift | 4 +- .../Sources/PeerListItemComponent.swift | 4 +- .../Sources/SendInviteLinkScreen.swift | 8 +- .../Sources/ArchiveInfoContentComponent.swift | 4 +- .../Sources/ArchiveInfoScreen.swift | 8 +- ...aticBusinessMessageListItemComponent.swift | 4 +- .../AutomaticBusinessMessageSetupScreen.swift | 11 +- .../Sources/BottomPanelComponent.swift | 4 +- .../BusinessLinkListItemComponent.swift | 10 +- .../Sources/BusinessLinksSetupScreen.swift | 11 +- .../QuickReplyEmptyStateComponent.swift | 4 +- .../Sources/QuickReplySetupScreen.swift | 15 +- .../Sources/BirthdayPickerComponent.swift | 4 +- .../BirthdayPickerContentComponent.swift | 4 +- .../Sources/BirthdayPickerScreen.swift | 9 +- .../Sources/BoostLevelIconComponent.swift | 4 +- .../Sources/BusinessDaySetupScreen.swift | 8 +- .../Sources/BusinessHoursSetupScreen.swift | 6 +- .../Sources/BusinessIntroSetupScreen.swift | 8 +- .../Sources/ChatIntroItemComponent.swift | 4 +- .../Sources/BusinessLocationSetupScreen.swift | 8 +- .../Sources/MapPreviewComponent.swift | 4 +- .../Sources/BusinessRecipientListScreen.swift | 6 +- .../ChatbotSearchResultItemComponent.swift | 12 +- .../Sources/ChatbotSetupScreen.swift | 6 +- .../Sources/CollectibleItemInfoScreen.swift | 12 +- ...ctibleItemInfoScreenContentComponent.swift | 4 +- .../NewSessionInfoContentComponent.swift | 4 +- .../Sources/NewSessionInfoScreen.swift | 8 +- .../Sources/ChannelAppearanceScreen.swift | 6 +- .../Sources/EmojiPickerItem.swift | 4 +- .../PeerNameColorProfilePreviewItem.swift | 2 +- .../Sources/PeerSelectionScreen.swift | 14 +- .../Sources/CategoryListItemComponent.swift | 4 +- .../CountriesMultiselectionScreen.swift | 14 +- .../Sources/CountryListItemComponent.swift | 4 +- .../Sources/OptionListItemComponent.swift | 4 +- .../Sources/SectionHeaderComponent.swift | 4 +- .../Sources/ShareWithPeersScreen.swift | 50 +- .../Sources/ShareWithPeersScreenState.swift | 6 - .../Sources/SliderComponent.swift | 49 +- .../Stars/StarsAvatarComponent/BUILD | 30 + .../Sources/StarsAvatarComponent.swift | 362 +++ .../Stars/StarsImageComponent/BUILD | 1 + .../Sources/StarsImageComponent.swift | 389 ++- .../Sources/ItemLoadingComponent.swift | 4 +- .../Sources/StarsPurchaseScreen.swift | 14 +- .../Stars/StarsTransactionScreen/BUILD | 40 + .../Sources/StarsTransactionScreen.swift | 216 +- .../Stars/StarsTransactionsScreen/BUILD | 7 + .../Sources/StarsBalanceComponent.swift | 186 +- .../Sources/StarsOverviewItemComponent.swift | 150 + .../Sources/StarsStatisticsScreen.swift | 795 +++++ .../StarsTransactionsListPanelComponent.swift | 299 +- ...sTransactionsPanelContainerComponent.swift | 32 +- .../Sources/StarsTransactionsScreen.swift | 58 +- .../Sources/StarsUtils.swift | 6 + .../Sources/StarsTransferScreen.swift | 205 +- .../Stars/StarsWithdrawalScreen/BUILD | 44 + .../StarsRevenueWithdrawalController.swift | 112 + .../Sources/StarsWithdrawalScreen.swift | 835 +++++ .../Sources/StickerPickerScreen.swift | 123 +- .../Sources/DataButtonComponent.swift | 4 +- .../Sources/DataCategoriesComponent.swift | 4 +- .../Sources/DataCategoryItemCompoment.swift | 8 +- .../Sources/DataUsageScreen.swift | 20 +- .../Sources/PieChartComponent.swift | 10 +- .../Sources/SegmentControlComponent.swift | 4 +- .../Sources/StorageCategoriesComponent.swift | 4 +- .../StorageCategoryItemCompoment.swift | 4 +- .../StorageFileListPanelComponent.swift | 16 +- .../Sources/StorageKeepSizeComponent.swift | 4 +- .../StorageMediaGridPanelComponent.swift | 8 +- .../StoragePeerListPanelComponent.swift | 16 +- .../StoragePeerTypeItemComponent.swift | 6 +- .../StorageUsagePanelContainerComponent.swift | 18 +- .../Sources/StorageUsageScreen.swift | 42 +- .../AvatarStoryIndicatorComponent.swift | 555 +++- .../Sources/ForwardInfoPanelComponent.swift | 4 +- .../Sources/PeerListItemComponent.swift | 10 +- .../Stories/StoryContainerScreen/BUILD | 1 + .../Sources/MediaAreaMaskView.swift | 45 + .../MediaNavigationStripComponent.swift | 6 +- .../Sources/OpenStories.swift | 4 +- .../Sources/StoryActionsComponent.swift | 8 +- .../Sources/StoryAuthorInfoComponent.swift | 4 +- .../Sources/StoryAvatarInfoComponent.swift | 4 +- .../Sources/StoryChatContent.swift | 476 +-- .../Sources/StoryContainerScreen.swift | 40 +- .../Sources/StoryContent.swift | 28 +- .../StoryContentCaptionComponent.swift | 331 +- .../StoryInteractionGuideComponent.swift | 8 +- .../Sources/StoryItemContentComponent.swift | 57 +- .../Sources/StoryItemImageView.swift | 6 +- .../Sources/StoryItemLoadingEffectView.swift | 2 +- .../Sources/StoryItemOverlaysView.swift | 8 +- .../StoryItemSetContainerComponent.swift | 444 ++- ...StoryItemSetContainerViewSendMessage.swift | 127 +- .../StoryItemSetViewListComponent.swift | 20 +- .../Sources/StoryPrivacyIconComponent.swift | 4 +- .../Sources/StoryFooterPanelComponent.swift | 4 +- .../Sources/StoryPeerListComponent.swift | 10 +- .../Sources/StoryPeerListItemComponent.swift | 18 +- .../TitleActivityIndicatorComponent.swift | 4 +- .../StoryQualityUpgradeSheetScreen.swift | 8 +- .../Sources/StorySetIndicatorComponent.swift | 172 +- ...StoryStealthModeInfoContentComponent.swift | 4 +- .../Sources/StoryStealthModeSheetScreen.swift | 12 +- .../Sources/SwitchComponent.swift | 4 +- .../Sources/TabSelectorComponent.swift | 4 +- .../Sources/TextFieldComponent.swift | 119 +- .../Sources/ToastContentComponent.swift | 4 +- .../Sources/TokenListTextField.swift | 6 +- .../Sources/LoadingEffectView.swift | 2 +- .../Sources/VideoMessageCameraScreen.swift | 18 +- .../Search Bar/Cashtag.imageset/Contents.json | 12 + .../Search Bar/Cashtag.imageset/cash_24.pdf | Bin 0 -> 1264 bytes .../Media Editor/Link.imageset/Contents.json | 12 + .../Media Editor/Link.imageset/link.pdf | Bin 0 -> 1419 bytes .../LinkLocked.imageset/Contents.json | 12 + .../LinkLocked.imageset/linklocked.pdf | Bin 0 -> 1886 bytes .../GroupingOff.imageset/Contents.json | 12 + .../GroupingOff.imageset/albumoff_24.pdf | Bin 0 -> 2114 bytes .../Media Grid/Lock.imageset/Contents.json | 12 + .../Media Grid/Lock.imageset/paidlock (3).pdf | Bin 0 -> 1174 bytes .../Media Grid/Paid.imageset/Contents.json | 12 + .../Paid.imageset/cash.circle_24.pdf | Bin 0 -> 1453 bytes .../MessageEffects.imageset/Contents.json | 12 + .../MessageEffects.imageset/msgeffects.pdf | Bin 0 -> 7485 bytes .../Contents.json | 12 + .../star8.pdf | Bin 0 -> 1701 bytes .../Contents.json | 12 + .../star24.pdf | Bin 0 -> 1420 bytes .../Stars/MediaLock.imageset/Contents.json | 12 + .../Stars/MediaLock.imageset/lockmedia.pdf | Bin 0 -> 1836 bytes .../Resources/Animations/anim_clock.json | 1 + .../TelegramUI/Sources/AccountContext.swift | 2 +- .../TelegramUI/Sources/AppDelegate.swift | 1 + .../Sources/ApplicationContext.swift | 9 + .../Chat/ChatControllerLoadDisplayNode.swift | 46 +- .../Chat/ChatControllerMediaRecording.swift | 6 +- ...hatControllerOpenBankCardContextMenu.swift | 85 + ...ChatControllerOpenCommandContextMenu.swift | 69 + ...ChatControllerOpenHashtagContextMenu.swift | 79 + .../ChatControllerOpenLinkContextMenu.swift | 205 ++ .../Chat/ChatControllerOpenLinkLongTap.swift | 40 + ...ChatControllerOpenMessageContextMenu.swift | 7 + .../ChatControllerOpenPhoneContextMenu.swift | 59 +- ...hatControllerOpenTimecodeContextMenu.swift | 69 + ...hatControllerOpenUsernameContextMenu.swift | 161 + .../Chat/ChatControllerOpenWebApp.swift | 412 +++ .../Sources/Chat/ChatControllerPaste.swift | 8 +- .../Chat/ChatMessageActionOptions.swift | 158 +- ...ChatMessageDisplaySendMessageOptions.swift | 28 +- ...UpdateChatPresentationInterfaceState.swift | 10 +- .../ChatBusinessLinkTitlePanelNode.swift | 6 +- .../TelegramUI/Sources/ChatController.swift | 1277 ++------ .../Sources/ChatControllerAdminBanUsers.swift | 111 +- .../ChatControllerForwardMessages.swift | 3 + .../Sources/ChatControllerNode.swift | 22 +- .../ChatControllerOpenAttachmentMenu.swift | 38 +- .../ChatControllerOpenCalendarSearch.swift | 2 +- ...rollerOpenMessageReactionContextMenu.swift | 82 +- .../ChatControllerOpenMessageShareMenu.swift | 2 +- .../Sources/ChatHistoryEntriesForView.swift | 8 +- .../Sources/ChatHistoryListNode.swift | 84 +- .../Sources/ChatHistoryViewForLocation.swift | 30 +- .../ChatInterfaceStateContextMenus.swift | 19 +- .../ChatManagingBotTitlePanelNode.swift | 6 +- .../Sources/ChatMessageTransitionNode.swift | 23 +- .../ChatPinnedMessageTitlePanelNode.swift | 52 +- .../ChatPremiumRequiredInputPanelNode.swift | 4 +- .../ChatRecordingPreviewInputPanelNode.swift | 6 + .../ChatSearchResultsContollerNode.swift | 2 +- .../ChatSearchTitleAccessoryPanelNode.swift | 6 +- .../Sources/ChatTagSearchInputPanelNode.swift | 24 +- .../ChatTextInputActionButtonsNode.swift | 190 +- .../Sources/ChatTextInputPanelNode.swift | 10 +- .../CommandChatInputContextPanelNode.swift | 6 +- .../Sources/CreateChannelController.swift | 1 + .../Sources/CreateGroupController.swift | 1 + .../Sources/NavigateToChatController.swift | 6 +- .../Sources/Nicegram/NGDeeplinkHandler.swift | 9 +- .../TelegramUI/Sources/OpenChatMessage.swift | 2 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 4 +- submodules/TelegramUI/Sources/OpenUrl.swift | 10 +- .../OverlayAudioPlayerControllerNode.swift | 3 +- .../Sources/PeerMessagesMediaPlaylist.swift | 6 +- .../Sources/SharedAccountContext.swift | 41 +- .../Sources/TelegramRootController.swift | 16 +- .../TransformOutgoingMessageMedia.swift | 18 + .../WebpagePreviewAccessoryPanelNode.swift | 8 +- .../Sources/ExperimentalUISettings.swift | 10 +- .../Sources/GroupCallContext.swift | 15 +- .../Sources/ChatTextInputAttributes.swift | 2 +- .../OngoingCallThreadLocalContext.h | 1 + .../Sources/OngoingCallThreadLocalContext.mm | 22 +- submodules/TgVoipWebrtc/tgcalls | 2 +- .../TooltipUI/Sources/TooltipScreen.swift | 2 +- .../Sources/PlayPauseIconComponent.swift | 4 +- .../Sources/TranslateButtonComponent.swift | 4 +- .../TranslateUI/Sources/TranslateScreen.swift | 16 +- .../Sources/UndoOverlayController.swift | 1 + .../Sources/UndoOverlayControllerNode.swift | 126 +- .../UrlEscaping/Sources/UrlEscaping.swift | 2 +- .../UrlHandling/Sources/UrlHandling.swift | 26 +- .../VolumeButtons/Sources/VolumeButtons.swift | 22 +- .../Sources/WatchRequestHandlers.swift | 2 +- .../WebUI/Sources/WebAppController.swift | 154 +- submodules/WebUI/Sources/WebAppWebView.swift | 16 + submodules/WebsiteType/BUILD | 3 +- .../WebsiteType/Sources/WebsiteType.swift | 27 + swift_deps.bzl | 14 +- swift_deps_index.json | 22 +- third-party/opus/build-opus-bazel.sh | 2 +- versions.json | 2 +- 1279 files changed, 78184 insertions(+), 29948 deletions(-) delete mode 100644 Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-20.png delete mode 100644 Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-20@2x.png delete mode 100644 Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-20@3x.png delete mode 100644 Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-29.png delete mode 100644 Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-29@2x.png delete mode 100644 Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-29@3x.png delete mode 100644 Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-40.png delete mode 100644 Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-40@2x.png delete mode 100644 Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-40@3x.png delete mode 100644 Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-60@2x.png delete mode 100644 Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-60@3x.png delete mode 100644 Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-76.png delete mode 100644 Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-76@2x.png delete mode 100644 Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-83.5@2x.png delete mode 100644 Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/Canvas.h delete mode 100644 Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/NullCanvasImpl.mm create mode 100644 Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SkiaCanvasImpl.cpp rename Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/{NullCanvasImpl.h => SkiaCanvasImpl.h} (55%) delete mode 100644 Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.h delete mode 100644 Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.mm create mode 100644 Tests/LottieMetalTest/skia/BUILD create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/OWNERS create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkAndroidCodec.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkAvifDecoder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkBmpDecoder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkCodec.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkCodecAnimation.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkEncodedImageFormat.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkEncodedOrigin.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkGifDecoder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkIcoDecoder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkJpegDecoder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkJpegxlDecoder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkPixmapUtils.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkPngChunkReader.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkPngDecoder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkRawDecoder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkWbmpDecoder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkWebpDecoder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/config/OWNERS create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/config/SkUserConfig.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkAlphaType.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkAnnotation.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkArc.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBBHFactory.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBitmap.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBlendMode.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBlender.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBlurTypes.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCanvas.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCanvasVirtualEnforcer.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCapabilities.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkClipOp.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColor.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorFilter.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorPriv.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorSpace.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorTable.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorType.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkContourMeasure.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCoverageMode.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCubicMap.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkData.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkDataTable.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkDocument.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkDrawable.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkExecutor.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFlattenable.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFont.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontArguments.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontMetrics.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontMgr.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontParameters.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontStyle.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontTypes.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkGraphics.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkImage.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkImageFilter.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkImageGenerator.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkImageInfo.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkM44.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMallocPixelRef.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMaskFilter.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMatrix.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMesh.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMilestone.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkOpenTypeSVGDecoder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkOverdrawCanvas.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPaint.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPath.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathBuilder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathEffect.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathMeasure.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathTypes.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathUtils.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPicture.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPictureRecorder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPixelRef.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPixmap.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPoint.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPoint3.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRRect.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRSXform.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRasterHandleAllocator.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRect.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRefCnt.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRegion.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSamplingOptions.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkScalar.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSerialProcs.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkShader.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSize.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSpan.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkStream.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkString.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkStrokeRec.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSurface.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSurfaceProps.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSwizzle.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTextBlob.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTextureCompressionType.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTileMode.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTiledImageUtils.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTraceMemoryDump.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTypeface.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTypes.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkUnPreMultiply.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkVertices.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkYUVAInfo.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkYUVAPixmaps.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/docs/SkMultiPictureDocument.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/docs/SkPDFDocument.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/docs/SkXPSDocument.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/Sk1DPathEffect.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/Sk2DPathEffect.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkBlenders.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkBlurMaskFilter.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkColorMatrix.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkColorMatrixFilter.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkCornerPathEffect.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkDashPathEffect.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkDiscretePathEffect.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkGradientShader.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkHighContrastFilter.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkImageFilters.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkLumaColorFilter.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkOverdrawColorFilter.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkPerlinNoiseShader.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkRuntimeEffect.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkShaderMaskFilter.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkTableMaskFilter.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkTrimPathEffect.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkEncoder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkICC.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkJpegEncoder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkPngEncoder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkWebpEncoder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/pathops/SkPathOps.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkCFObject.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontConfigInterface.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_FontConfigInterface.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_Fontations.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_android.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_data.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_directory.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_empty.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_fontconfig.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_fuchsia.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_mac_ct.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkImageGeneratorCG.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkImageGeneratorNDK.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkImageGeneratorWIC.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkTypeface_fontations.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkTypeface_mac.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkTypeface_win.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/OWNERS create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkColorData.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkEncodedInfo.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkExif.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkGainmapInfo.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkGainmapShader.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkIDChangeListener.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkJpegGainmapEncoder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkJpegMetadataDecoder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkPathRef.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkSLSampleUsage.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkWeakRefCnt.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkXmp.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/README.md create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SingleOwner.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAPI.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkASAN.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAlign.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAlignedStorage.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAnySubclass.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAssert.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAttributes.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkCPUTypes.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkContainers.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkDebug.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkDeque.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkFeatures.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkFixed.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkFloatingPoint.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkLoadUserConfig.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkMacros.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkMalloc.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkMath.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkMutex.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkNoncopyable.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkOnce.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkPoint_impl.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkSafe32.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkSemaphore.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkSpan_impl.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTArray.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTDArray.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTFitsIn.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTLogic.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTPin.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTemplates.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkThreadAnnotations.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkThreadID.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTo.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTypeTraits.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrDeferredDisplayList.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrDeferredDisplayListRecorder.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrPromiseImageTexture.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrSurfaceCharacterization.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrVkSecondaryCBDrawContext.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/SkChromeRemoteGlyphCache.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/SkDiscardableMemory.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/SkImageChromium.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/Slug.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrContext_Base.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrD3DTypesMinimal.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrImageContext.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrTextureGenerator.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrTypesPriv.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/graphite/ContextOptionsPriv.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/graphite/DawnTypesPriv.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/graphite/MtlGraphiteTypesPriv.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/graphite/VulkanGraphiteTypesPriv.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/vk/SkiaVulkan.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/sksl/OWNERS create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/sksl/SkSLDebugTrace.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/sksl/SkSLVersion.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/svg/SkSVGCanvas.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkCamera.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkCanvasStateUtils.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkCustomTypeface.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkEventTracer.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkNWayCanvas.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkNoDrawCanvas.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkNullCanvas.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkOrderedFontMgr.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkPaintFilterCanvas.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkParse.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkParsePath.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkShadowUtils.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkTextUtils.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkTraceEventPhase.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/mac/SkCGUtils.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/BUILD.gn create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/OWNERS create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/README.chromium create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/skcms.cc create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/skcms.gni create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/skcms.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/Transform_inl.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_Transform.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_TransformBaseline.cc create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_TransformHsw.cc create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_TransformSkx.cc create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_internals.h create mode 100644 Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_public.h create mode 100755 Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/version.sha1 create mode 100644 Tests/LottieMetalTest/skia/device/libskia.framework/Info.plist create mode 100755 Tests/LottieMetalTest/skia/device/libskia.framework/libskia create mode 100644 Tests/LottieMetalTest/skia/simulator/libskia.framework/Info.plist create mode 100755 Tests/LottieMetalTest/skia/simulator/libskia.framework/libskia delete mode 100644 submodules/BrowserUI/Sources/BrowserStackContainerNode.swift create mode 100644 submodules/ContextUI/Sources/ReactionPreviewView.swift create mode 100644 submodules/Display/Source/Navigation/MinimizedContainer.swift create mode 100644 submodules/DrawingUI/Sources/DrawingLinkEntityView.swift create mode 100644 submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift create mode 100644 submodules/LottieCpp/BUILD create mode 160000 submodules/LottieCpp/lottiecpp rename submodules/{PeerInfoUI => PremiumUI}/Sources/IncreaseLimitFooterItem.swift (94%) rename submodules/{PeerInfoUI => PremiumUI}/Sources/IncreaseLimitHeaderItem.swift (92%) create mode 100644 submodules/StatisticsUI/Sources/StarsTransactionItem.swift delete mode 100644 submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift create mode 100644 submodules/TelegramCore/Sources/ApiUtils/TelegramExtendedMedia.swift create mode 100644 submodules/TelegramCore/Sources/Statistics/StarsRevenueStatistics.swift create mode 100644 submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaPaidContent.swift create mode 100644 submodules/TelegramCore/Sources/TelegramEngine/Messages/ExtendedMedia.swift create mode 100644 submodules/TelegramUI/Components/Calls/VoiceChatActionButton/BUILD create mode 100644 submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Metal/VoiceChatActionButtonShaders.metal create mode 100644 submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/BlobView.swift create mode 100644 submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceBlobView.swift create mode 100644 submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceChatActionButton.swift create mode 100644 submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceChatActionButtonBackgroundNode.swift create mode 100644 submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceChatActionButtonIconNode.swift create mode 100644 submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceChatRaiseHandNode.swift create mode 100644 submodules/TelegramUI/Components/Calls/VoiceChatActionButton/VoiceChatActionButtonAssets.xcassets/Contents.json create mode 100644 submodules/TelegramUI/Components/Calls/VoiceChatActionButton/VoiceChatActionButtonAssets.xcassets/VoiceChatActionButton/Contents.json create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageStarsMediaInfoNode/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageStarsMediaInfoNode/Sources/ChatMessageStarsMediaInfoNode.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageUnlockMediaNode/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageUnlockMediaNode/Sources/ChatMessageUnlockMediaNode.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/BadgeLabelView.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift delete mode 100644 submodules/TelegramUI/Components/LottieCpp/BUILD delete mode 100644 submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/BezierPath.h delete mode 100644 submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CGPath.h delete mode 100644 submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CGPathCocoa.h delete mode 100644 submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/Color.h delete mode 100644 submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CurveVertex.h delete mode 100644 submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimation.h delete mode 100644 submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimationContainer.h delete mode 100644 submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieCpp.h delete mode 100644 submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieRenderTree.h delete mode 100644 submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/PathElement.h delete mode 100644 submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h delete mode 100644 submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/ShapeAttributes.h delete mode 100644 submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/Vectors.h delete mode 100644 submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/VectorsCocoa.h delete mode 100644 submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/lottiejson11.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeUtils/BezierPathUtils.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeUtils/BezierPathUtils.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerFontProvider.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerFontProvider.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerImageProvider.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerImageProvider.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerTextProvider.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerTextProvider.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerTransformNode.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerTransformNode.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasRenderUpdates.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasUpdate.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PassThroughOutputNode.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Protocols/RenderNode.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/RenderLayers/GetGradientParameters.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/RenderLayers/GetGradientParameters.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Animation.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Animation.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/Asset.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/Asset.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/AssetLibrary.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/AssetLibrary.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/ImageAsset.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/ImageAsset.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/PrecompAsset.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/PrecompAsset.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Keyframes/KeyframeGroup.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Keyframes/KeyframeGroup.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ImageLayerModel.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ImageLayerModel.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModel.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModel.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/PreCompLayerModel.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/PreCompLayerModel.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ShapeLayerModel.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ShapeLayerModel.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/SolidLayerModel.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/SolidLayerModel.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/TextLayerModel.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/TextLayerModel.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/DashElement.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/DashElement.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/FitzModifier.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/FitzModifier.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Marker.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Marker.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Mask.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Mask.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Transform.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Transform.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Ellipse.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Ellipse.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Fill.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Fill.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientFill.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientFill.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientStroke.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientStroke.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Group.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Group.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Merge.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Merge.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Rectangle.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Rectangle.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Repeater.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Repeater.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/RoundedRectangle.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/RoundedRectangle.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Shape.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Shape.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeItem.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeItem.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeTransform.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeTransform.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Star.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Star.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Stroke.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Stroke.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Font.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Font.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Glyph.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Glyph.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextAnimator.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextAnimator.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextDocument.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextDocument.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/BezierPath.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CompoundBezierPath.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CompoundBezierPath.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CoordinateSpace.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CoordinateSpace.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CurveVertex.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/PathElement.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/DynamicProperties/AnimationKeypath.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/DynamicProperties/AnimationKeypath.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/DynamicProperties/AnyValueProvider.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/DynamicProperties/AnyValueProvider.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/FontProvider/AnimationFontProvider.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/FontProvider/AnimationFontProvider.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/ImageProvider/AnimationImageProvider.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Interpolatable.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Interpolatable.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Keyframe.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Keyframe.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnimationTime.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnimationTime.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnyValue.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnyValue.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayer.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayer.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.mm delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CTFont.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CTFont.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Color.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/DashPattern.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/DashPattern.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/GradientColorSet.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/GradientColorSet.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Vectors.mm delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/TextProvider/AnimationTextProvider.cpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/TextProvider/AnimationTextProvider.hpp delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimation.mm delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainerInternal.h delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationInternal.h delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.h delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.mm delete mode 100644 submodules/TelegramUI/Components/LottieCpp/Sources/lottiejson11/lottiejson11.cpp create mode 100644 submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingLinkEntity.swift create mode 100644 submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkOptions.swift create mode 100644 submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift create mode 100644 submodules/TelegramUI/Components/MinimizedContainer/BUILD create mode 100644 submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift create mode 100644 submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift create mode 100644 submodules/TelegramUI/Components/MinimizedContainer/Sources/Utils.swift create mode 100644 submodules/TelegramUI/Components/PeerManagement/OldChannelsController/BUILD rename submodules/{PeerInfoUI => TelegramUI/Components/PeerManagement/OldChannelsController}/Sources/OldChannelsController.swift (99%) rename submodules/{PeerInfoUI => TelegramUI/Components/PeerManagement/OldChannelsController}/Sources/OldChannelsSearch.swift (98%) create mode 100644 submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD rename submodules/{PeerInfoUI => TelegramUI/Components/PeerManagement/OwnershipTransferController}/Sources/ChannelOwnershipTransferController.swift (98%) rename submodules/TelegramUI/{ => Components/PeerManagement/OwnershipTransferController}/Sources/OwnershipTransferController.swift (93%) create mode 100644 submodules/TelegramUI/Components/Stars/StarsAvatarComponent/BUILD create mode 100644 submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift create mode 100644 submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD rename submodules/TelegramUI/Components/Stars/{StarsTransactionsScreen => StarsTransactionScreen}/Sources/StarsTransactionScreen.swift (83%) create mode 100644 submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift create mode 100644 submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift create mode 100644 submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsUtils.swift create mode 100644 submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD create mode 100644 submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsRevenueWithdrawalController.swift create mode 100644 submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift create mode 100644 submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaAreaMaskView.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Components/Search Bar/Cashtag.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Components/Search Bar/Cashtag.imageset/cash_24.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Link.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Link.imageset/link.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/LinkLocked.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/LinkLocked.imageset/linklocked.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Grid/GroupingOff.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Grid/GroupingOff.imageset/albumoff_24.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Grid/Lock.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Grid/Lock.imageset/paidlock (3).pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Grid/Paid.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Grid/Paid.imageset/cash.circle_24.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Perk/MessageEffects.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Perk/MessageEffects.imageset/msgeffects.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/SendStarsPeerBadgeStarIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/SendStarsPeerBadgeStarIcon.imageset/star8.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/SendStarsStarSliderIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/SendStarsStarSliderIcon.imageset/star24.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/MediaLock.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/MediaLock.imageset/lockmedia.pdf create mode 100644 submodules/TelegramUI/Resources/Animations/anim_clock.json create mode 100644 submodules/TelegramUI/Sources/Chat/ChatControllerOpenBankCardContextMenu.swift create mode 100644 submodules/TelegramUI/Sources/Chat/ChatControllerOpenCommandContextMenu.swift create mode 100644 submodules/TelegramUI/Sources/Chat/ChatControllerOpenHashtagContextMenu.swift create mode 100644 submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift create mode 100644 submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkLongTap.swift create mode 100644 submodules/TelegramUI/Sources/Chat/ChatControllerOpenTimecodeContextMenu.swift create mode 100644 submodules/TelegramUI/Sources/Chat/ChatControllerOpenUsernameContextMenu.swift create mode 100644 submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift diff --git a/.bazelrc b/.bazelrc index 5325dfd50d8..43ccf92e1f2 100644 --- a/.bazelrc +++ b/.bazelrc @@ -11,8 +11,10 @@ build --per_file_copt="third-party/webrtc/.*\.cpp$","@-std=c++17" build --per_file_copt="third-party/webrtc/.*\.cc$","@-std=c++17" build --per_file_copt="third-party/webrtc/.*\.mm$","@-std=c++17" build --per_file_copt="submodules/LottieMeshSwift/LottieMeshBinding/Sources/.*\.mm$","@-std=c++17" -build --per_file_copt="submodules/TelegramUI/Components/LottieCpp/Sources/.*\.mm$","@-std=c++17" -build --per_file_copt="submodules/TelegramUI/Components/LottieCpp/Sources/.*\.cpp$","@-std=c++17" +build --per_file_copt="submodules/LottieCpp/lottiecpp/Sources/.*\.mm$","@-std=c++17" +build --per_file_copt="submodules/LottieCpp/lottiecpp/Sources/.*\.cpp$","@-std=c++17" +build --per_file_copt="submodules/LottieCpp/lottiecpp/PlatformSpecific/Darwin/Sources/.*\.mm$","@-std=c++17" +build --per_file_copt="submodules/LottieCpp/lottiecpp/PlatformSpecific/Darwin/Sources/.*\.cpp$","@-std=c++17" build --per_file_copt="Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/.*\.cpp$","@-std=c++17" build --per_file_copt="Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/.*\.mm$","@-std=c++17" diff --git a/.gitmodules b/.gitmodules index cfb5c5bd621..a96a095b824 100644 --- a/.gitmodules +++ b/.gitmodules @@ -31,3 +31,6 @@ [submodule "build-system/bazel-rules/rules_xcodeproj"] path = build-system/bazel-rules/rules_xcodeproj url = https://github.com/MobileNativeFoundation/rules_xcodeproj.git +[submodule "submodules/LottieCpp/lottiecpp"] + path = submodules/LottieCpp/lottiecpp + url = https://github.com/ali-fareed/lottiecpp.git diff --git a/Nicegram/NGUI/Sources/NicegramSettingsController.swift b/Nicegram/NGUI/Sources/NicegramSettingsController.swift index dda92f6f046..0312fd39a2b 100644 --- a/Nicegram/NGUI/Sources/NicegramSettingsController.swift +++ b/Nicegram/NGUI/Sources/NicegramSettingsController.swift @@ -536,7 +536,9 @@ private enum NicegramSettingsControllerEntry: ItemListNodeEntry { return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: section) case let .unblock(text, url): return ItemListActionItem(presentationData: presentationData, title: text, kind: .neutral, alignment: .natural, sectionId: section, style: .blocks) { - CoreContainer.shared.urlOpener().open(url) + Task { @MainActor in + CoreContainer.shared.urlOpener().open(url) + } } case let .Account(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: section) diff --git a/Package.resolved b/Package.resolved index 9e0a14c2782..f098b928566 100644 --- a/Package.resolved +++ b/Package.resolved @@ -33,7 +33,7 @@ "location" : "https://github.com/denis15yo/core-swift.git", "state" : { "branch" : "release/1.0.0", - "revision" : "20b7275f60ad80634f056905d7f18292294cd510" + "revision" : "87eb31396b7ef8962686b1c8de561950efc2673f" } }, { @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/scenee/FloatingPanel", "state" : { - "revision" : "22d46c526084724a718b8c39ab77f12452712cc7", - "version" : "2.8.3" + "revision" : "29185a47bd9f062c060e097641b863ef07f60ba7", + "version" : "2.8.4" } }, { @@ -123,7 +123,7 @@ "location" : "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", "state" : { "branch" : "develop", - "revision" : "0985fd5dfae1676121c54c31fe2817059d5bf784" + "revision" : "1b98221095de9a80b44e93ce9bfc699c720d5c00" } }, { @@ -132,7 +132,7 @@ "location" : "git@bitbucket.org:mobyrix/nicegram-wallet-ios.git", "state" : { "branch" : "develop", - "revision" : "241a210ee1b5cb9cb95d9245a4ed384973ee2ef2" + "revision" : "3573f8ef65b8667b5bc927bfa558ffb39f2bd579" } }, { @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SDWebImage/SDWebImage.git", "state" : { - "revision" : "b8523c1642f3c142b06dd98443ea7c48343a4dfd", - "version" : "5.19.3" + "revision" : "be0bcd7823ce56629948491f2eaeaa19979514f7", + "version" : "5.19.4" } }, { @@ -221,8 +221,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "ee97538f5b81ae89698fd95938896dec5217b148", - "version" : "1.1.1" + "revision" : "3d2dc41a01f9e49d84f0a3925fb858bed64f702d", + "version" : "1.1.2" } }, { @@ -338,8 +338,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/trustwallet/wallet-core.git", "state" : { - "revision" : "94116a24445c2052edbc7203baf68296c68ce8f4", - "version" : "4.0.46" + "revision" : "352e0833678151ae124e1e70d325fc056d76d150", + "version" : "4.0.49" } }, { diff --git a/Random.txt b/Random.txt index 30983f8bad9..397d9dd9335 100644 --- a/Random.txt +++ b/Random.txt @@ -1 +1 @@ -3a64b94cc76109006741756f85402c85 +3a64b94cc76109006741756f85403c85 diff --git a/Telegram/SiriIntents/IntentMessages.swift b/Telegram/SiriIntents/IntentMessages.swift index 37360288e34..3369b10bde7 100644 --- a/Telegram/SiriIntents/IntentMessages.swift +++ b/Telegram/SiriIntents/IntentMessages.swift @@ -53,7 +53,7 @@ func unreadMessages(account: Account) -> Signal<[INMessage], NoError> { } if !isMuted && hasUnread { - signals.append(account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: index.messageIndex.id.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 10, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: .combinedLocation) + signals.append(account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: index.messageIndex.id.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: 10, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: .combinedLocation) |> take(1) |> map { view -> [INMessage] in var messages: [INMessage] = [] diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-20.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-20.png deleted file mode 100644 index 3392b4bfc664028cb5c12910acb41307a309f306..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1905 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 diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-20@3x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-20@3x.png deleted file mode 100644 index 153b910e21dcd31c6cb915d2624748a06e341c75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4390 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 diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-29@3x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-29@3x.png deleted file mode 100644 index 081aee7ffba63a96e2d0016d9925c889cbf41f3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6444 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 diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-40.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-40.png deleted file mode 100644 index 1757c2ec338730608bcddf4760c250876d1334e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3055 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 diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-40@2x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-40@2x.png deleted file mode 100644 index fccd23b779f0be7d3f64969ebbd734bf2674c9ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5891 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 Z|9{(*!483xQfdGI002ovPDHLkV1hJ|YtjG! diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-40@3x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-40@3x.png deleted file mode 100644 index efc75b73a9072e1d9f7c64f08aeb2a72092e0caa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9230 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 diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-60@2x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-60@2x.png deleted file mode 100644 index efc75b73a9072e1d9f7c64f08aeb2a72092e0caa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9230 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 diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-60@3x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-60@3x.png deleted file mode 100644 index 40dbad9c3eda89ec519b337710b57622ad4f29f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15938 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 diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-76.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-76.png deleted file mode 100644 index 76dd37112a42dab2c39a13331d298e9824c55e2f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5561 zcmV;q6-MfbP)BE1ZQLxAY_vg;$?V;oX(lt{{G@3qJ`_O^!KmPq;Qw2}Cn0L9?Tam?khm;cmfj@-EE=vY<#k5o0f^m&*sM6pBI0EL{uRZtU4R5Z z`^X8`V)zNfJ8YaF;u6EP09P2E5Ad?jnUOMxUqvh^s*L``+lU9}BlcPZ2XpY-jyyG} zttleQMaed+noc9ry@UIIwng1*#M^^UkFzj+6yF25_LnMhmf?pGpH<}CbcnAmX|u%; z|7E#p3GpKUUAw%6atiT2#A8)^yd3Cssj4^8z$>TJnP(8+gLrkpJj%xIMEtceUz`ec zeo~wa<_{hK;NmJRuzL3+9o0iVC08s$)vACv7|KExH6jmbd4gn8^0FkllzkRT6fd|P0n zrwSVAXrbt(XrcJk;OyVRDf#{Yw^VBW$m{6QFhZvPBk<6w!tZERBfJcLd1$M3&)TehHS09YRtkN(Z(GDi^F2;vKBX;HY+Q*UEnTqUvgh@A9J5aYSek}G;8j! zqp=F+X_rylpF&#LEj%b>g+0Q~5^H*5N-?MwSLAe5T{?GXqX;9WfX;*4pXw6&V*io?m|<6;W0GAb4f<-njQ>$>Q>9MPr@ zwU=n+0I+*9IKpp9{{0u*d8K7@p9VPmCp-6AwmA>rtOL;dXv;Q$XXCx^0IWWyc=l3w zC&Xgk0yx!!!@*Zi8qcdA>-AfBZYACT*nGWS|NH%Vee(m<-vGFv{{#Oecnig2*2@3@ z061k>NoGw=04e|g00;m9hiL!=000010000Q0000000N)_00aO40096108F3*00aO4 z00961089V?001>{<=OxM5h6)MK~#7F?OSP(T}2f>Z<#lfOfp#}`ywG5Nm#QG77fIvkpg;H58EVPuh3Pp)aq*5XUSOS8Agi$}W*!w5>lo$lLx`a9=*eY$V=y)UWO*48rBm{O@!RZmx)2t~Dt zOn>I(a=h!dO!~uSV1LHb#^z z+QQ^r^#NgVp!K#xd4D+H8#`TuNdK_5iYXZ;Bp^w0V1(gBwI#vsE15Vu!t96s3c~uz zpg5c;gW~$u(;i0@u;Xm2nv&9?mg%FN5ggRHu;SV^R=s7dD-0`%4Ek0P76$l1){C(& zV(ZZ>xKS8MWOV;f!T?mS1HJ98x@2puvoUR*ak#ya&n6LujBQk$5n=6GUteGX zLt}b+v9$@apuYNGO7;aQ#Zotw$_1);t;mMMsZ6GcEv;w?9Ic1A3bL4`QlXRe@NUVx z++^ZPB}%2Ul*$CI*2P$26frE!IS@5@>^U@P%t9(+)R`OaD$$;I*V5K)tJzRQYW3DH z3|s2MJk+*8wp=Pw>->vp=Zl9 z>UOTXCfgZ8Rw!^+J z7rJQ3j72p0g8Qk=UX@s&gh0y?HIA7@+wNLSRP5%8>H)K{qVAo&{OIgf%o|P$Z z!+OgdL<2_6plr)%Diw>7y2ErxJ#;wK4)(~BFwM+p2353yWn40nO( zwAr`N^vM@fp>Vjua2reXyXw&<$!<@T*dZP-=!MSUD)J3SJr7ZZUv_`Njmkp_i|Q2C zmgR;0RPuf|PM!|N!f4C^u%21&}CGed?Xmkw8*XhfTCk0eZYl5$+0PGzW2Izmsq zb_MNy?{&)gqb|{gsX4fDBrW>nuc&446e<*Xq2VM(Ngk)i)vgH=eG+Y2UUW!~HeGhz zOaDZKg#p+~0?X!rYT>B-lwqEfz-vew!H(#E&W zp#`V@jIx;lR4Q@kaS}XRk7bQk;*1Wq7bcweHl7u+9kI#Ai62ch9L>BbnVqG3zED(`w z`)yIQ*F=~Nr0;E|4IDlH*!g36Y3tq8$j<~rmhXrmjU(;&qc5aWPq>w%teYbY-g!w# zlU(ZU+-vuqlE5@%aUKxJW%ku1+ddl$_hB;FNZ0%!Hw?7e0GuOpv}W_SX~Ub3nI|Wn zmNN2ij`zGYf5!DRd&UjCu{}~@u-2o?qDR-6b<2+^nTjT79SAWY&F-r)bXm)x9+!zQ zmyigD<<-X{-qo|`I8)jQ$0j}&(^KoOrd|793AP(%divz0xZ9b>-%2wlEoIWYjjiBl z`*c~_gIuO06Qm;zL}HHy(!!Dp7j<@hFeUAVFkq>-<%_&%k8YYtt&LNdIPa+^vdGh^ zEOi#%r{Ar+jNUu6lN!W1i8ju&dnH=31Zb6A*^VFd^MRm&NFOsgq_6UQ8>dtfA|t{h4O8ETZm`x0BcM8GeJXzhf&s zzHT{ncOLY%8*mU80+X*M-qMHqlxM#U<`mVhG! z+o>Vx5p>DKU(m4JC!EtWmu;qXZL8_|Ew`DeDVt4MsLjTsI+6vK%~eNxr;fdq5A}@UZw)k#DRW;T zOh=eRY{ZTNZX`~L2m_in(1mUmTt7lgr1|6(nY2Z%*U?$Suc0n~tDyU5(hbz!y_+7{ zu#DO}-l^PfU^*evczT{b?FKq|s`tiL_lI3x@)v#I2Aedzh}Eg5pRSEBCnijwv?3|X z8fJFzR+M2wiig3H(YMoVjy7yJq@Q5opx)O08a=x43XaMnJ=+anfi%oSCjK+CzDvhW zTuR;fcIjG?n+RiL07J)^FcyS-ae8!fXJ!H~SuoELqb)WJ4)O7mvN4rZk_< z)6+ZtXhU;+P~Wuw5A@{bud$P5Z}Eroe#DqHvXrIIop>)DJ^F0wRf#>b4 zK^R{`oIWdJ#Bt0Vo0&w*nY8B2Fy5nq1^rOrgL=A=p51XXz1sF$GM|2Zhe4WPwB$z7 z7v`>_VFSH4wy>v!wG7(|Okve0h%kg;FH)6{e&3o1tJcd4E*pyV%8(?v7_M?W1deH( zM@z^2gfe`-#GxDs24Z4)2lX;NzU3O)w*RG`gF1-t$-qNkRPz)=+JIau^TeRs*KAy& zi=l!@_xst4K6KWS`iL+l3x+aLAWeufkp}Vjh%lc(W3Z5@>JVuQhg?OAhOKa(IWzp7bARVH`pxUhsI&8c`SN52g^7?z z!>53mqZZNPNMkZmxDZ_+ zl8whDqb)g>uqDH9p;KBeq^{Co4Qa{^=>fFk(2Mlw)~k8J&l}RvR}%-)=1#nbK0V_* zR4(!lVW5)>D7G?^FjV7I`8P)O+Vn6CC`I2 z)AP*9H*!R7HE%o8FoyNuXB5zthdrENLcJ-Nl5xz&DKd|A7BZyqYpe_|9(|djLA3c% zCtD6g*`V@Zx? zgn%@&e(7}$B*7{F(oa!51g2(Bpyea@J7ubor!yGnN!V^+v-|M2E9uRH>!{Htgh`QT z(nLvFBCH3|3lM#ur0gV2I`)n@@7N&30T`F8YnY-Y+I-T4xL}jZ{NR<)yrxBT!SFkI zcg8Al zqxxe?13x)mKlltix^o3D=;h$oL1wXMoM0e)@^If*DIiP$H3?Fu^Qd-!Rjgrruj@dZ zAqfuynW9W$1#wK4w|J-N(xG?Ku}uqj?P7+ll;f{UFYQ}Ne|+l>v!1#3d3st}-79XR zeA0rDt0D{nXpl>H2rKi9V;~PIh$gPnJ__))MejC&E(8(Y-Ly_EN* z+srOlMgTe6Y%aJ9b3J1HdcHy6U>cSDO)fUN3~erKbQ`!+SFt^mF!`eX$gZ!@u!e~= zW9S(?g*|+;Y%vCYecs-Eh(979U>n3bHYO&C-?mt$v9a-HyE5!vNV3~AJP~)Lx6tU) zbZRY+=9i-d>ZUI97p1)G;$Q9Naq%g-z?Uhr9OQ8o`PyFm7d>?N8`NHY*L*Os_2M2s zW5)~0&4-?+v5hBEb9Ol6<#{jH#aB1A=l9Ul+rLF`9DJD%>0W$jKf=l`T}EF^OAC)f z@}w1@my86q0T{s%rE&Z>6Vz)NJ+-QoA_UEaKVD=UDDPp~IvA}Xh-lO3_=AthY2J1W z$&TU9J(1K|JVb}{dzmbJV)2spA*?KZNgv5my(J7CwGs^sLQ4K5)b*kab5fHQ)cb{C zong*A{xX~kfnIL+(f39_HoTbEe!?K?e@vlm7a8QJmo}H}QWv|1X;3Mnvw$NPFgS`S&+pQ}vDl==!f>SC)yJ-@i7;(MSXmPpCKFdLj=1D>o9z_j zb-Sy7T%8LOeQG8w3{V@QjZPwuzPgXbvdc;F?0!k^qp)ixEQw+*lavVEr$5TJT~TDg zo_-^&FP*oY6zn)>MNV>!gTDG+WbFJSM_80o(w2lcmu!-L9~r{LL3_kq_aYlbKvbIx zQ|f|$!YoRp;lq@RZEzB0+gVrL2W5W{VPb?B_dzMF1y^iCUtJa~SDm!=b>4G^@R6as zKMm+>hdyY$Ai``#nzRpN)Y*kB22_TCa%Vf>;L0)6`w=bFo~+T zT!6IP|H7IFzg*juUQJ0&N>mxjG?BU-)vv$XTy|;OUG4t?zdiA~^npzL00000NkvXX Hu0mjfuZgyo diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-76@2x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-76@2x.png deleted file mode 100644 index 269edaf719a673c9f3b3effd82386637db4d6263..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12431 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 N002ovPDHLkV1fhvANK$N diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-83.5@2x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/1-83.5@2x.png deleted file mode 100644 index 0a18bb2283f555e636e36cb5032883f958c6e1ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14187 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 BDDnUR diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Contents.json b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Contents.json index 6d71a29b721..4c3df491cca 100644 --- a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Contents.json +++ b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Contents.json @@ -1,116 +1,14 @@ { "images" : [ - { - "size" : "20x20", - "idiom": "iphone", - "filename" : "1-20@2x.png", - "scale": "2x" - }, - { - "size" : "20x20", - "idiom": "iphone", - "filename" : "1-20@3x.png", - "scale": "3x" - }, - { - "size" : "20x20", - "idiom": "ipad", - "filename" : "1-20.png", - "scale": "1x" - }, { - "size" : "20x20", - "idiom": "ipad", - "filename" : "1-20@2x.png", - "scale": "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "1-29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "1-29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "1-40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "1-40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "1-60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "1-60@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "1-29.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "1-29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "1-40.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "1-40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "1-76.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "1-76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "1-83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", "filename" : "1-1024.png", - "scale" : "1x" + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } } diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index cf39a2058ce..9a6e7054cd1 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -108,6 +108,7 @@ "PUSH_MESSAGE_NOTHEME" = "%1$@|disabled chat theme"; "PUSH_MESSAGE_RECURRING_PAY" = "%1$@|You were charged %2$@"; "CHAT_MESSAGE_RECURRING_PAY" = "%1$@|You were charged %2$@"; +"PUSH_MESSAGE_PAID_MEDIA" = "%1$@sent you a paid post for %2$@ stars"; "PUSH_CHANNEL_MESSAGE_TEXT" = "%1$@|%2$@"; "PUSH_CHANNEL_MESSAGE_NOTEXT" = "%1$@|posted a message"; @@ -138,6 +139,7 @@ "PUSH_CHANNEL_ALBUM" = "%1$@|posted an album"; "PUSH_CHANNEL_MESSAGE_DOCS_TEXT_1" = "posted a file"; "PUSH_CHANNEL_MESSAGE_DOCS_TEXT_any" = "posted %d files"; +"PUSH_CHANNEL_MESSAGE_PAID_MEDIA" = "%1$@ sent a paid post for %2$@ stars"; "PUSH_CHAT_MESSAGE_TEXT" = "%2$@|%1$@:%3$@"; "PUSH_CHAT_MESSAGE_NOTEXT" = "%2$@|%1$@ sent a message to the group"; @@ -183,6 +185,7 @@ "PUSH_CHAT_MESSAGE_THEME" = "%1$@|set theme to %3$@ in the group %2$@"; "PUSH_CHAT_MESSAGE_NOTHEME" = "%1$@|disabled theme in the group %2$@"; "PUSH_CHAT_REQ_JOINED" = "%1$@ was accepted into the group %2$@"; +"PUSH_CHAT_MESSAGE_PAID_MEDIA" = "%1$@ sent a paid post to the group %2$@ for %3$@ stars"; "PUSH_PINNED_TEXT" = "%1$@|pinned \"%2$@\" "; "PUSH_PINNED_NOTEXT" = "%1$@|pinned a message"; @@ -200,6 +203,7 @@ "PUSH_PINNED_GAME" = "%1$@|pinned a game"; "PUSH_PINNED_INVOICE" = "%1$@|pinned an invoice"; "PUSH_PINNED_GIF" = "%1$@|pinned a GIF"; +"PUSH_PINNED_PAID_MEDIA" = "%1$@|pinned a paid post for %2$@"; "PUSH_CONTACT_JOINED" = "%1$@|joined Nicegram!"; @@ -252,6 +256,7 @@ "PUSH_CHAT_REACT_GAME" = "%2$@|%1$@ %3$@ to your game"; "PUSH_CHAT_REACT_INVOICE" = "%2$@|%1$@ %3$@ to your invoice"; "PUSH_CHAT_REACT_GIF" = "%2$@|%1$@ %3$@ to your GIF"; +"PUSH_CHAT_REACT_PAID_MEDIA" = "%2$@|%1$@ %3$@ to your paid post"; "PUSH_MESSAGE_SUGGEST_USERPIC" = "%1$@|suggested you new profile photo"; @@ -273,6 +278,7 @@ "PUSH_REACT_STORY" = "%1$@|%2$@ to your story"; "PUSH_REACT_STORY_HIDDEN" = "New reaction to your story"; + "LOCAL_MESSAGE_FWDS" = "%1$@ forwarded you %2$d messages"; "LOCAL_CHANNEL_MESSAGE_FWDS" = "%1$@ posted %2$d forwarded messages"; "LOCAL_CHAT_MESSAGE_FWDS" = "%1$@ forwarded %2$d messages"; @@ -10629,6 +10635,7 @@ Sorry for the inconvenience."; "MediaEditor.Shortcut.Location" = "Location"; "MediaEditor.Shortcut.Reaction" = "Reaction"; "MediaEditor.Shortcut.Audio" = "Audio"; +"MediaEditor.Shortcut.Link" = "Link"; "BoostGift.AdditionalPrizes" = "Additional Prizes"; "BoostGift.AdditionalPrizesPlaceholder" = "Enter Your Prize"; @@ -12203,6 +12210,7 @@ Sorry for the inconvenience."; "HashtagSearch.NoResults" = "No Results"; "HashtagSearch.NoResultsQueryDescription" = "There were no results for %@.\nTry another hashtag."; +"HashtagSearch.NoResultsQueryCashtagDescription" = "There were no results for %@.\nTry another cashtag."; "Chat.Context.Phone.AddToContacts" = "Add to Contacts"; "Chat.Context.Phone.CreateNewContact" = "Create New Contact"; @@ -12216,6 +12224,21 @@ Sorry for the inconvenience."; "Chat.Context.Phone.NotOnTelegram" = "This number is not on Telegram."; "Chat.Context.Phone.ViewProfile" = "View Profile"; +"Chat.Context.Username.SendMessage" = "Send Message"; +"Chat.Context.Username.OpenGroup" = "Open Group"; +"Chat.Context.Username.OpenChannel" = "Open Channel"; +"Chat.Context.Username.Copy" = "Copy Username"; +"Chat.Context.Username.NotOnTelegram" = "This user doesn't exist on Telegram."; + +"Chat.Context.Hashtag.Search" = "Search"; +"Chat.Context.Hashtag.Copy" = "Copy Hashtag"; + +"Chat.Context.Card.Copy" = "Copy Card Number"; + +"Chat.Context.Command.Copy" = "Copy Command"; + +"Chat.Context.Timecode.Copy" = "Copy Timecode"; + "Message.FactCheck" = "Fact Check"; "Message.FactCheck.WhatIsThis" = "what's this?"; @@ -12238,6 +12261,7 @@ Sorry for the inconvenience."; "Stars.Intro.Incoming" = "Incoming"; "Stars.Intro.Outgoing" = "Outgoing"; +"Stars.Intro.Transaction.MediaPurchase" = "Media Purchase"; "Stars.Intro.Transaction.AppleTopUp.Title" = "Stars Top-Up"; "Stars.Intro.Transaction.AppleTopUp.Subtitle" = "via App Store"; "Stars.Intro.Transaction.GoogleTopUp.Title" = "Stars Top-Up"; @@ -12246,6 +12270,10 @@ Sorry for the inconvenience."; "Stars.Intro.Transaction.PremiumBotTopUp.Subtitle" = "via Premium Bot"; "Stars.Intro.Transaction.FragmentTopUp.Title" = "Stars Top-Up"; "Stars.Intro.Transaction.FragmentTopUp.Subtitle" = "via Fragment"; +"Stars.Intro.Transaction.FragmentWithdrawal.Title" = "Withdrawal"; +"Stars.Intro.Transaction.FragmentWithdrawal.Subtitle" = "via Fragment"; +"Stars.Intro.Transaction.TelegramAds.Title" = "Withdrawal"; +"Stars.Intro.Transaction.TelegramAds.Subtitle" = "via Telegram Ads"; "Stars.Intro.Transaction.Unsupported.Title" = "Unsupported"; "Stars.Intro.Transaction.Refund" = "Refund"; @@ -12262,6 +12290,7 @@ Sorry for the inconvenience."; "Stars.Purchase.StarsNeeded_1" = "%@ Star Needed"; "Stars.Purchase.StarsNeeded_any" = "%@ Stars Needed"; "Stars.Purchase.StarsNeededInfo" = "Buy Stars to use them on **%@** and other miniapps."; +"Stars.Purchase.StarsNeededUnlockInfo" = "Buy Stars to unlock media and use them on miniapps."; "Stars.Purchase.Stars_1" = "%@ Star"; "Stars.Purchase.Stars_any" = "%@ Stars"; @@ -12272,12 +12301,14 @@ Sorry for the inconvenience."; "Stars.Transaction.Via" = "Via"; "Stars.Transaction.To" = "To"; "Stars.Transaction.From" = "From"; +"Stars.Transaction.Media" = "Media"; "Stars.Transaction.Id" = "Transaction ID"; "Stars.Transaction.Date" = "Date"; "Stars.Transaction.Terms" = "Review the [Terms of Service]() for Stars."; "Stars.Transaction.Terms_URL" = "https://telegram.org/tos"; "Stars.Transaction.CopiedId" = "Transaction ID copied to clipboard."; +"Stars.Transaction.MediaPurchase" = "Media Purchase"; "Stars.Transaction.AppleTopUp.Title" = "Stars Top-Up"; "Stars.Transaction.AppleTopUp.Subtitle" = "App Store"; "Stars.Transaction.GoogleTopUp.Title" = "Stars Top-Up"; @@ -12286,9 +12317,21 @@ Sorry for the inconvenience."; "Stars.Transaction.PremiumBotTopUp.Subtitle" = "Premium Bot"; "Stars.Transaction.FragmentTopUp.Title" = "Stars Top-Up"; "Stars.Transaction.FragmentTopUp.Subtitle" = "Fragment"; +"Stars.Transaction.FragmentWithdrawal.Title" = "Stars Withdrawal"; +"Stars.Transaction.FragmentWithdrawal.Subtitle" = "Fragment"; +"Stars.Transaction.TelegramAds.Title" = "Stars Withdrawal"; +"Stars.Transaction.TelegramAds.Subtitle" = "Telegram Ads"; "Stars.Transaction.Unsupported.Title" = "Unsupported"; "Stars.Transaction.Refund" = "Refund"; +"Stars.Transaction.Photos_1" = "%@ Photo"; +"Stars.Transaction.Photos_any" = "%@ Photos"; +"Stars.Transaction.Videos_1" = "%@ Video"; +"Stars.Transaction.Videos_any" = "%@ Videos"; +"Stars.Transaction.SinglePhoto" = "Photo"; +"Stars.Transaction.SingleVideo" = "Video"; +"Stars.Transaction.MediaAnd" = "%1$@ and %2$@"; + "Stars.Transfer.Title" = "Confirm Your Purchase"; "Stars.Transfer.Info" = "Do you want to buy **%1$@** in **%2$@** for **%3$@**?"; "Stars.Transfer.Info.Stars_1" = "%@ Star"; @@ -12298,11 +12341,21 @@ Sorry for the inconvenience."; "Stars.Transfer.PurchasedText" = "You acquired **%1$@** in **%2$@** for **%3$@**."; "Stars.Transfer.Purchased.Stars_1" = "%@ Star"; "Stars.Transfer.Purchased.Stars_any" = "%@ Stars"; +"Stars.Transfer.UnlockedText" = "You unlocked media for **%1$@**."; +"Stars.Transfer.UnlockInfo" = "Do you want to unlock %1$@ in **%2$@** for **%3$@**?"; "Stars.Transfer.Balance" = "Balance"; "Stars.Transfer.Unavailable" = "Sorry, no star purchases are available from your country."; +"Stars.Transfer.Photos_1" = "%@ photo"; +"Stars.Transfer.Photos_any" = "%@ photos"; +"Stars.Transfer.Videos_1" = "%@ video"; +"Stars.Transfer.Videos_any" = "%@ videos"; +"Stars.Transfer.SinglePhoto" = "photo"; +"Stars.Transfer.SingleVideo" = "video"; +"Stars.Transfer.MediaAnd" = "%1$@ and %2$@"; + "Settings.Stars" = "Your Stars"; "Chat.MessageEffectMenu.TitleAddEffect" = "Add an animated effect"; @@ -12314,3 +12367,114 @@ Sorry for the inconvenience."; "BusinessLink.AlertTextLimitText" = "The message text limit is 4096 characters"; "Chat.SendMessageMenu.EditMessage" = "Edit Message"; + +"Story.ViewLink" = "Open Link"; + +"PeerInfo.Bot.Username" = "Username"; +"PeerInfo.Bot.Balance" = "Balance"; +"PeerInfo.Bot.Balance.Stars_1" = "%@ Star"; +"PeerInfo.Bot.Balance.Stars_any" = "%@ Stars"; + +"HashtagSearch.StoriesFound_1" = "%@ Story Found"; +"HashtagSearch.StoriesFound_any" = "%@ Stories Found"; +"HashtagSearch.StoriesFoundInfo" = "View stories with %@"; + +"Stars.BotRevenue.Title" = "Stars Balance"; +"Stars.BotRevenue.Revenue.Title" = "Stars Received"; +"Stars.BotRevenue.Proceeds.Title" = "Rewards Overview"; +"Stars.BotRevenue.Proceeds.Available" = "Available Balance"; +"Stars.BotRevenue.Proceeds.Current" = "Total Balance"; +"Stars.BotRevenue.Proceeds.Total" = "Lifetime Proceeds"; +"Stars.BotRevenue.Proceeds.Info" = "Stars from your total balance can be used for ads or withdrawn as rewards 21 days after they are earned."; + +"Stars.BotRevenue.Withdraw.Balance" = "Available Balance"; +"Stars.BotRevenue.Withdraw.Withdraw" = "Withdraw via Fragment"; +"Stars.BotRevenue.Withdraw.WithdrawShort" = "Withdraw"; +"Stars.BotRevenue.Withdraw.BuyAds" = "Buy Ads"; +"Stars.BotRevenue.Withdraw.Info" = "You can collect rewards for Stars using Fragment, or use Stars to advertise your bot. [Learn More >]()"; +"Stars.BotRevenue.Withdraw.Info_URL" = "https://telegram.org/tos/bot-developers#6-2-2-tpa-balance"; + +"Stars.BotRevenue.Transactions.Title" = "Transaction History"; + +"Stars.Withdraw.Title" = "Withdraw"; +"Stars.Withdraw.AmountTitle" = "ENTER AMOUNT TO WITHDRAW"; +"Stars.Withdraw.AmountPlaceholder" = "Stars Amount"; +"Stars.Withdraw.Withdraw" = "Withdraw"; + +"Stars.Withdraw.Withdraw.ErrorMinimum" = "You cannot withdraw less than [%@]()."; +"Stars.Withdraw.Withdraw.ErrorMinimum.Stars_1" = "%@ Star"; +"Stars.Withdraw.Withdraw.ErrorMinimum.Stars_any" = "%@ Stars"; + +"Stars.Withdraw.Withdraw.ErrorTimeout" = "Next withdrawal will be available in **%@**."; + +"Stars.PaidContent.Title" = "Paid Content"; +"Stars.PaidContent.AmountTitle" = "ENTER UNLOCK COST"; +"Stars.PaidContent.AmountPlaceholder" = "Stars to Unlock"; +"Stars.PaidContent.AmountInfo" = "Users will have to transfer this amount of Stars to your channel in order to view this media.\n[More about Stars >]()"; +"Stars.PaidContent.AmountInfo_URL" = "https://telegram.org/tos/stars"; +"Stars.PaidContent.Create" = "Make This Media Paid"; + +"MediaEditor.AddLink" = "LINK"; + +"MediaEditor.Link.CreateTitle" = "Create Link"; +"MediaEditor.Link.EditTitle" = "Edit Link"; +"MediaEditor.Link.LinkTo.Title" = "LINK TO"; +"MediaEditor.Link.LinkTo.Placeholder" = "https://somesite.com"; +"MediaEditor.Link.LinkName.Title" = "LINK NAME (OPTIONAL)"; +"MediaEditor.Link.LinkName.Placeholder" = "Enter a Name"; + +"Story.Editor.TooltipLinkPremium" = "Subscribe to [Telegram Premium]() to add links."; + +"Story.Editor.TooltipLinkLimitValue_1" = "**%@** link"; +"Story.Editor.TooltipLinkLimitValue_any" = "**%@** links"; +"Story.Editor.TooltipReachedLinkLimitText" = "You can't add more than %@ to a story."; + +"VoiceChat.ToastMicrophoneIsMuted" = "Your microphone is muted."; + +"StoryGridScreen.TitleLocationSearch" = "Location"; +"StoryList.GridHeaderLocationSearch_1" = "1 STORY FROM THIS LOCATION"; +"StoryList.GridHeaderLocationSearch_any" = "%d STORIES FROM THIS LOCATION"; + +"Chat.MessagesDeletedToast.Text_1" = "Message Deleted"; +"Chat.MessagesDeletedToast.Text_any" = "%d Messages Deleted"; + +"Monetization.StarsRevenueTitle" = "REVENUE"; + +"Monetization.TonBalanceTitle" = "TON BALANCE"; +"Monetization.StarsBalanceTitle" = "STARS BALANCE"; + +"Monetization.TonTransactions" = "TON Transactions"; +"Monetization.StarsTransactions" = "Stars Transactions"; + +"Monetization.BalanceStarsWithdraw" = "Withdraw via Fragment"; +"Monetization.BalanceStarsWithdrawShort" = "Withdraw"; +"Monetization.BalanceStarsBuyAds" = "Buy Ads"; +"Monetization.Balance.StarsInfo" = "You can collect rewards for Stars using Fragment, or use Stars to advertise your channel. [Learn More >]()"; +"Monetization.Balance.StarsInfo_URL" = "https://telegram.org"; + +"Monetization.StarsProceeds.Title" = "Rewards Overview"; +"Monetization.StarsProceeds.Available" = "Available Balance"; +"Monetization.StarsProceeds.Current" = "Total Balance"; +"Monetization.StarsProceeds.Total" = "Lifetime Proceeds"; + +"Premium.MessageEffects" = "Message Effects"; +"Premium.MessageEffectsInfo" = "Add over 500 animated effects to private messages."; + +"Chat.PaidMedia.UnlockMedia" = "Unlock for %@"; +"Chat.PaidMedia.Purchased" = "Purchased"; + +"Attachment.SendWithoutGrouping" = "Send Without Grouping"; +"Attachment.Paid.EditPrice" = "Edit Price"; +"Attachment.Paid.EditPrice.Stars_1" = "%@ Star"; +"Attachment.Paid.EditPrice.Stars_any" = "%@ Stars"; +"Attachment.Paid.Create" = "Make This Content Paid"; + +"WebApp.MinimizedTitleFormat" = "%1$@ & %2$@"; +"WebApp.MinimizedTitle.Others_1" = "%@ Other"; +"WebApp.MinimizedTitle.Others_any" = "%@ Others"; + +"Stars.SendStars.Title" = "Send Stars"; +"Stars.SendStars.AmountTitle" = "ENTER AMOUNT"; +"Stars.SendStars.AmountPlaceholder" = "Stars Amount"; +"Stars.SendStars.AmountInfo" = "Send %@ or more to highlight your profile in the TOP 3 supporters of this message."; +"Stars.SendStars.SendStars" = "Confirm and Send"; diff --git a/Tests/LottieMetalTest/BUILD b/Tests/LottieMetalTest/BUILD index 1e9987af9f3..4226bfd67d6 100644 --- a/Tests/LottieMetalTest/BUILD +++ b/Tests/LottieMetalTest/BUILD @@ -49,7 +49,7 @@ swift_library( deps = [ "//submodules/Display", "//submodules/MetalEngine", - "//submodules/TelegramUI/Components/LottieCpp", + "//submodules/LottieCpp", "//submodules/TelegramUI/Components/LottieMetal", "//submodules/rlottie:RLottieBinding", "//Tests/LottieMetalTest/QOILoader", @@ -201,6 +201,7 @@ ios_application( resources = [ "//Tests/Common:LaunchScreen", ":TestDataBundle", + "//Tests/LottieMetalTest/skia", ], frameworks = [ ], diff --git a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/CGFloatExtensions.swift b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/CGFloatExtensions.swift index 939725a331a..bd7d6f32e8e 100644 --- a/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/CGFloatExtensions.swift +++ b/Tests/LottieMetalTest/LottieSwift/Sources/Private/Utility/Extensions/CGFloatExtensions.swift @@ -8,6 +8,53 @@ import Foundation import QuartzCore +private func fma(_ a: CGFloat, _ b: CGFloat, _ c: CGFloat) -> CGFloat { + return a * b + c +} + +private func eval_poly(_ t: CGFloat, _ b: CGFloat) -> CGFloat { + return b; +} + +private func eval_poly(_ t: CGFloat, _ m: CGFloat, _ b: CGFloat) -> CGFloat { + return eval_poly(t, fma(m, t, b)) +} + +private func eval_poly(_ t: CGFloat, _ m: CGFloat, _ b: CGFloat, _ c: CGFloat) -> CGFloat { + return eval_poly(t, fma(m, t, b), c) +} + +private func eval_poly(_ t: CGFloat, _ m: CGFloat, _ b: CGFloat, _ c: CGFloat, _ d: CGFloat) -> CGFloat { + return eval_poly(t, fma(m, t, b), c, d) +} + +private func cubic_solver(_ A: CGFloat, _ B: CGFloat, _ C: CGFloat, _ D: CGFloat) -> CGFloat { + var t = -D + + for _ in 0 ..< 8 { + let f = eval_poly(t, A, B, C, D) // f = At^3 + Bt^2 + Ct + D + if (abs(f) <= 0.00005) { + break; + } + let fp = eval_poly(t, 3.0 * A, 2.0 * B, C) // f' = 3At^2 + 2Bt + C + let fpp = eval_poly(t, 3.0 * A + 3.0 * A, 2.0 * B) // f'' = 6At + 2B + + let numer = 2.0 * fp * f + let denom = fma(2 * fp, fp, -(f * fpp)) + + t -= numer / denom + } + + if t < 0.0 { + t = 0.0 + } + if t > 1.0 { + t = 1.0 + } + + return t +} + extension CGFloat { // MARK: Internal @@ -90,7 +137,9 @@ extension CGFloat { } fileprivate static func SolveCubic(_ a: CGFloat, _ b: CGFloat, _ c: CGFloat, _ d: CGFloat) -> CGFloat { - if a == 0 { + return cubic_solver(a, b, c, d) + + /*if a == 0 { return SolveQuadratic(b, c, d) } if d == 0 { @@ -151,6 +200,6 @@ extension CGFloat { } } - return -1; + return -1;*/ } } diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/BUILD b/Tests/LottieMetalTest/SoftwareLottieRenderer/BUILD index 0aedc6e7b23..041c8eda84c 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/BUILD +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/BUILD @@ -23,8 +23,10 @@ objc_library( "PublicHeaders", ], deps = [ - "//submodules/TelegramUI/Components/LottieCpp", + "//submodules/LottieCpp", "//Tests/LottieMetalTest/thorvg", + "//Tests/LottieMetalTest/skia", + "//Tests/LottieMetalTest/skia:libskia" ], sdk_frameworks = [ "Foundation", diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/PublicHeaders/SoftwareLottieRenderer/SoftwareLottieRenderer.h b/Tests/LottieMetalTest/SoftwareLottieRenderer/PublicHeaders/SoftwareLottieRenderer/SoftwareLottieRenderer.h index 42939022507..960b2d3450c 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/PublicHeaders/SoftwareLottieRenderer/SoftwareLottieRenderer.h +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/PublicHeaders/SoftwareLottieRenderer/SoftwareLottieRenderer.h @@ -4,8 +4,6 @@ #import #import -#import - #ifdef __cplusplus extern "C" { #endif @@ -14,9 +12,14 @@ CGRect getPathNativeBoundingBox(CGPathRef _Nonnull path); @interface SoftwareLottieRenderer : NSObject -- (instancetype _Nonnull)initWithAnimationContainer:(LottieAnimationContainer * _Nonnull)animationContainer; +@property (nonatomic, readonly) NSInteger frameCount; +@property (nonatomic, readonly) NSInteger framesPerSecond; +@property (nonatomic, readonly) CGSize size; + +- (instancetype _Nullable)initWithData:(NSData * _Nonnull)data; -- (UIImage * _Nullable)renderForSize:(CGSize)size useReferenceRendering:(bool)useReferenceRendering; +- (void)setFrame:(CGFloat)index; +- (UIImage * _Nullable)renderForSize:(CGSize)size useReferenceRendering:(bool)useReferenceRendering canUseMoreMemory:(bool)canUseMoreMemory skipImageGeneration:(bool)skipImageGeneration; @end diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/Canvas.h b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/Canvas.h deleted file mode 100644 index 32a02bfc8aa..00000000000 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/Canvas.h +++ /dev/null @@ -1,92 +0,0 @@ -#ifndef Canvas_h -#define Canvas_h - -#include - -#include -#include -#include -#include - -namespace lottieRendering { - -class Image { -public: - virtual ~Image() = default; -}; - -class Gradient { -public: - Gradient(std::vector const &colors, std::vector const &locations) : - _colors(colors), - _locations(locations) { - assert(_colors.size() == _locations.size()); - } - - std::vector const &colors() const { - return _colors; - } - - std::vector const &locations() const { - return _locations; - } - -private: - std::vector _colors; - std::vector _locations; -}; - -enum class BlendMode { - Normal, - DestinationIn, - DestinationOut -}; - -enum class PathCommandType { - MoveTo, - LineTo, - CurveTo, - Close -}; - -typedef struct { - PathCommandType type; - CGPoint points[4]; -} PathCommand; - -typedef std::function)> CanvasPathEnumerator; - -class Canvas { -public: - virtual ~Canvas() = default; - - virtual int width() const = 0; - virtual int height() const = 0; - - virtual std::shared_ptr makeLayer(int width, int height) = 0; - - virtual void saveState() = 0; - virtual void restoreState() = 0; - - virtual void fillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Color const &color) = 0; - virtual void linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) = 0; - virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) = 0; - - virtual void strokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, lottie::Color const &color) = 0; - virtual void linearGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) = 0; - virtual void radialGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) = 0; - - virtual void fill(lottie::CGRect const &rect, lottie::Color const &fillColor) = 0; - virtual void setBlendMode(BlendMode blendMode) = 0; - - virtual void setAlpha(float alpha) = 0; - - virtual void concatenate(lottie::Transform2D const &transform) = 0; - - virtual void draw(std::shared_ptr const &other, lottie::CGRect const &rect) = 0; -}; - -} - -#endif - diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.h b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.h index db8064458a5..cc5f5e00492 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.h +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.h @@ -1,70 +1,61 @@ -#ifndef CoreGraphicsCanvasImpl_h -#define CoreGraphicsCanvasImpl_h +#ifndef CoreGraphicsCoreGraphicsCanvasImpl_h +#define CoreGraphicsCoreGraphicsCanvasImpl_h -#include "Canvas.h" +#include -namespace lottieRendering { +#include -class ImageImpl: public Image { -public: - ImageImpl(::CGImageRef image); - virtual ~ImageImpl(); - ::CGImageRef nativeImage() const; - -private: - CGImageRef _image = nil; -}; +namespace lottie { -class CanvasImpl: public Canvas { -public: - CanvasImpl(int width, int height); - CanvasImpl(CGContextRef context, int width, int height); - virtual ~CanvasImpl(); +class CoreGraphicsCanvasImpl: public Canvas { +class Layer; - virtual int width() const override; - virtual int height() const override; +public: + class Image { + public: + Image(::CGImageRef image); + virtual ~Image(); + ::CGImageRef nativeImage() const; + + private: + CGImageRef _image = nil; + }; - std::shared_ptr makeLayer(int width, int height) override; +public: + CoreGraphicsCanvasImpl(int width, int height); + virtual ~CoreGraphicsCanvasImpl(); virtual void saveState() override; virtual void restoreState() override; virtual void fillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Color const &color) override; virtual void linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override; - virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override; + virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, Vector2D const ¢er, float radius) override; virtual void strokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, lottie::Color const &color) override; virtual void linearGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override; virtual void radialGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override; - virtual void fill(lottie::CGRect const &rect, lottie::Color const &fillColor) override; - virtual void setBlendMode(BlendMode blendMode) override; - virtual void setAlpha(float alpha) override; + virtual void clip(CGRect const &rect) override; + virtual bool clipPath(CanvasPathEnumerator const &enumeratePath, FillRule fillRule, Transform2D const &transform) override; virtual void concatenate(lottie::Transform2D const &transform) override; - virtual std::shared_ptr makeImage() const; - virtual void draw(std::shared_ptr const &other, lottie::CGRect const &rect) override; + virtual std::shared_ptr makeImage(); - CGContextRef nativeContext() const { - return _context; - } + virtual bool pushLayer(CGRect const &rect, float alpha, std::optional maskMode) override; + virtual void popLayer() override; - std::vector &backingData() { - return _backingData; - } + std::vector &backingData(); + int bytesPerRow(); - int bytesPerRow() { - return _bytesPerRow; - } +private: + std::shared_ptr ¤tLayer(); private: int _width = 0; int _height = 0; - int _bytesPerRow = 0; - std::vector _backingData; - CGContextRef _context = nil; CGContextRef _topContext = nil; - CGLayerRef _layer = nil; + std::vector> _layerStack; }; } diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.mm b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.mm index 38f95beb724..fe9016c9b45 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.mm +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/CoreGraphicsCanvasImpl.mm @@ -1,6 +1,9 @@ #include "CoreGraphicsCanvasImpl.h" -namespace lottieRendering { +#include +#include + +namespace lottie { namespace { @@ -59,117 +62,136 @@ bool addEnumeratedPath(CGContextRef context, CanvasPathEnumerator const &enumera } -ImageImpl::ImageImpl(::CGImageRef image) { +class CoreGraphicsCanvasImpl::Layer { +public: + struct Composition { + CGRect rect; + float alpha; + Transform2D transform; + std::optional maskMode; + + Composition(CGRect rect_, float alpha_, Transform2D transform_, std::optional maskMode_) : + rect(rect_), alpha(alpha_), transform(transform_), maskMode(maskMode_) { + } + }; + +public: + explicit Layer(int width, int height, std::optional composition) { + _width = width; + _height = height; + _composition = composition; + + _bytesPerRow = alignUp(width * 4, 16); + _backingData.resize(_bytesPerRow * _height); + memset(_backingData.data(), 0, _backingData.size()); + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst; + _context = CGBitmapContextCreate(_backingData.data(), _width, _height, 8, _bytesPerRow, colorSpace, bitmapInfo); + CFRelease(colorSpace); + + CGContextClearRect(_context, CGRectMake(0.0, 0.0, _width, _height)); + } + + ~Layer() { + CGContextRelease(_context); + } + + CGContextRef context() const { + return _context; + } + + std::optional composition() const { + return _composition; + } + + std::shared_ptr makeImage() { + ::CGImageRef nativeImage = CGBitmapContextCreateImage(_context); + if (nativeImage) { + auto image = std::make_shared(nativeImage); + CFRelease(nativeImage); + return image; + } else { + return nil; + } + } + +public: + CGContextRef _context = nil; + int _width = 0; + int _height = 0; + int _bytesPerRow = 0; + std::vector _backingData; + + std::optional _composition; +}; + +CoreGraphicsCanvasImpl::Image::Image(::CGImageRef image) { _image = CGImageRetain(image); } -ImageImpl::~ImageImpl() { +CoreGraphicsCanvasImpl::Image::~Image() { CFRelease(_image); } -::CGImageRef ImageImpl::nativeImage() const { +::CGImageRef CoreGraphicsCanvasImpl::Image::nativeImage() const { return _image; } - -CanvasImpl::CanvasImpl(int width, int height) { - _width = width; - _height = height; - _bytesPerRow = alignUp(width * 4, 16); - _backingData.resize(_bytesPerRow * _height); - memset(_backingData.data(), 0, _backingData.size()); - - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - - CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst; - _context = CGBitmapContextCreate(_backingData.data(), _width, _height, 8, _bytesPerRow, colorSpace, bitmapInfo); - - CGContextClearRect(_context, CGRectMake(0.0, 0.0, _width, _height)); - - //CGContextSetInterpolationQuality(_context, kCGInterpolationLow); - //CGContextSetAllowsAntialiasing(_context, true); - //CGContextSetShouldAntialias(_context, true); - - CFRelease(colorSpace); - - _topContext = CGContextRetain(_context); -} - -CanvasImpl::CanvasImpl(CGContextRef context, int width, int height) { - _topContext = CGContextRetain(context); - _layer = CGLayerCreateWithContext(context, CGSizeMake(width, height), nil); - _context = CGContextRetain(CGLayerGetContext(_layer)); - _width = width; - _height = height; -} - -CanvasImpl::~CanvasImpl() { - CFRelease(_context); - if (_topContext) { - CFRelease(_topContext); - } - if (_layer) { - CFRelease(_layer); - } -} - -int CanvasImpl::width() const { - return _width; +CoreGraphicsCanvasImpl::CoreGraphicsCanvasImpl(int width, int height) : +_width(width), +_height(height) { + _layerStack.push_back(std::make_shared(width, height, std::nullopt)); } -int CanvasImpl::height() const { - return _height; +CoreGraphicsCanvasImpl::~CoreGraphicsCanvasImpl() { } -std::shared_ptr CanvasImpl::makeLayer(int width, int height) { - return std::make_shared(_topContext, width, height); +void CoreGraphicsCanvasImpl::saveState() { + CGContextSaveGState(currentLayer()->context()); } -void CanvasImpl::saveState() { - CGContextSaveGState(_context); +void CoreGraphicsCanvasImpl::restoreState() { + CGContextRestoreGState(currentLayer()->context()); } -void CanvasImpl::restoreState() { - CGContextRestoreGState(_context); -} - -void CanvasImpl::fillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Color const &color) { - if (!addEnumeratedPath(_context, enumeratePath)) { +void CoreGraphicsCanvasImpl::fillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Color const &color) { + if (!addEnumeratedPath(currentLayer()->context(), enumeratePath)) { return; } CGFloat components[4] = { color.r, color.g, color.b, color.a }; - CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(_topContext), components); - CGContextSetFillColorWithColor(_context, nativeColor); + CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(currentLayer()->context()), components); + CGContextSetFillColorWithColor(currentLayer()->context(), nativeColor); CFRelease(nativeColor); switch (fillRule) { case lottie::FillRule::EvenOdd: { - CGContextEOFillPath(_context); + CGContextEOFillPath(currentLayer()->context()); break; } default: { - CGContextFillPath(_context); + CGContextFillPath(currentLayer()->context()); break; } } } -void CanvasImpl::linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) { - CGContextSaveGState(_context); +void CoreGraphicsCanvasImpl::linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) { + CGContextSaveGState(currentLayer()->context()); - if (!addEnumeratedPath(_context, enumeratePath)) { - CGContextRestoreGState(_context); + if (!addEnumeratedPath(currentLayer()->context(), enumeratePath)) { + CGContextRestoreGState(currentLayer()->context()); return; } switch (fillRule) { case lottie::FillRule::EvenOdd: { - CGContextEOClip(_context); + CGContextEOClip(currentLayer()->context()); break; } default: { - CGContextClip(_context); + CGContextClip(currentLayer()->context()); break; } } @@ -191,31 +213,31 @@ bool addEnumeratedPath(CGContextRef context, CanvasPathEnumerator const &enumera locations.push_back(location); } - CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(_topContext), components.data(), locations.data(), locations.size()); + CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(currentLayer()->context()), components.data(), locations.data(), locations.size()); if (nativeGradient) { - CGContextDrawLinearGradient(_context, nativeGradient, CGPointMake(start.x, start.y), CGPointMake(end.x, end.y), kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); + CGContextDrawLinearGradient(currentLayer()->context(), nativeGradient, CGPointMake(start.x, start.y), CGPointMake(end.x, end.y), kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); CFRelease(nativeGradient); } - CGContextResetClip(_context); - CGContextRestoreGState(_context); + CGContextResetClip(currentLayer()->context()); + CGContextRestoreGState(currentLayer()->context()); } -void CanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) { - CGContextSaveGState(_context); +void CoreGraphicsCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, Vector2D const ¢er, float radius) { + CGContextSaveGState(currentLayer()->context()); - if (!addEnumeratedPath(_context, enumeratePath)) { - CGContextRestoreGState(_context); + if (!addEnumeratedPath(currentLayer()->context(), enumeratePath)) { + CGContextRestoreGState(currentLayer()->context()); return; } switch (fillRule) { case lottie::FillRule::EvenOdd: { - CGContextEOClip(_context); + CGContextEOClip(currentLayer()->context()); break; } default: { - CGContextClip(_context); + CGContextClip(currentLayer()->context()); break; } } @@ -237,62 +259,62 @@ bool addEnumeratedPath(CGContextRef context, CanvasPathEnumerator const &enumera locations.push_back(location); } - CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(_topContext), components.data(), locations.data(), locations.size()); + CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(currentLayer()->context()), components.data(), locations.data(), locations.size()); if (nativeGradient) { - CGContextDrawRadialGradient(_context, nativeGradient, CGPointMake(startCenter.x, startCenter.y), startRadius, CGPointMake(endCenter.x, endCenter.y), endRadius, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); + CGContextDrawRadialGradient(currentLayer()->context(), nativeGradient, CGPointMake(center.x, center.y), 0.0, CGPointMake(center.x, center.y), radius, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); CFRelease(nativeGradient); } - CGContextResetClip(_context); - CGContextRestoreGState(_context); + CGContextResetClip(currentLayer()->context()); + CGContextRestoreGState(currentLayer()->context()); } -void CanvasImpl::strokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, lottie::Color const &color) { - if (!addEnumeratedPath(_context, enumeratePath)) { +void CoreGraphicsCanvasImpl::strokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, lottie::Color const &color) { + if (!addEnumeratedPath(currentLayer()->context(), enumeratePath)) { return; } CGFloat components[4] = { color.r, color.g, color.b, color.a }; - CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(_topContext), components); - CGContextSetStrokeColorWithColor(_context, nativeColor); + CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(currentLayer()->context()), components); + CGContextSetStrokeColorWithColor(currentLayer()->context(), nativeColor); CFRelease(nativeColor); - CGContextSetLineWidth(_context, lineWidth); + CGContextSetLineWidth(currentLayer()->context(), lineWidth); switch (lineJoin) { case lottie::LineJoin::Miter: { - CGContextSetLineJoin(_context, kCGLineJoinMiter); + CGContextSetLineJoin(currentLayer()->context(), kCGLineJoinMiter); break; } case lottie::LineJoin::Round: { - CGContextSetLineJoin(_context, kCGLineJoinRound); + CGContextSetLineJoin(currentLayer()->context(), kCGLineJoinRound); break; } case lottie::LineJoin::Bevel: { - CGContextSetLineJoin(_context, kCGLineJoinBevel); + CGContextSetLineJoin(currentLayer()->context(), kCGLineJoinBevel); break; } default: { - CGContextSetLineJoin(_context, kCGLineJoinBevel); + CGContextSetLineJoin(currentLayer()->context(), kCGLineJoinBevel); break; } } switch (lineCap) { case lottie::LineCap::Butt: { - CGContextSetLineCap(_context, kCGLineCapButt); + CGContextSetLineCap(currentLayer()->context(), kCGLineCapButt); break; } case lottie::LineCap::Round: { - CGContextSetLineCap(_context, kCGLineCapRound); + CGContextSetLineCap(currentLayer()->context(), kCGLineCapRound); break; } case lottie::LineCap::Square: { - CGContextSetLineCap(_context, kCGLineCapSquare); + CGContextSetLineCap(currentLayer()->context(), kCGLineCapSquare); break; } default: { - CGContextSetLineCap(_context, kCGLineCapSquare); + CGContextSetLineCap(currentLayer()->context(), kCGLineCapSquare); break; } } @@ -302,54 +324,54 @@ bool addEnumeratedPath(CGContextRef context, CanvasPathEnumerator const &enumera for (const auto value : dashPattern) { mappedDashPattern.push_back(value); } - CGContextSetLineDash(_context, dashPhase, mappedDashPattern.data(), mappedDashPattern.size()); + CGContextSetLineDash(currentLayer()->context(), dashPhase, mappedDashPattern.data(), mappedDashPattern.size()); } - CGContextStrokePath(_context); + CGContextStrokePath(currentLayer()->context()); } -void CanvasImpl::linearGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) { - CGContextSaveGState(_context); - if (!addEnumeratedPath(_context, enumeratePath)) { - CGContextRestoreGState(_context); +void CoreGraphicsCanvasImpl::linearGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) { + CGContextSaveGState(currentLayer()->context()); + if (!addEnumeratedPath(currentLayer()->context(), enumeratePath)) { + CGContextRestoreGState(currentLayer()->context()); return; } - CGContextSetLineWidth(_context, lineWidth); + CGContextSetLineWidth(currentLayer()->context(), lineWidth); switch (lineJoin) { case lottie::LineJoin::Miter: { - CGContextSetLineJoin(_context, kCGLineJoinMiter); + CGContextSetLineJoin(currentLayer()->context(), kCGLineJoinMiter); break; } case lottie::LineJoin::Round: { - CGContextSetLineJoin(_context, kCGLineJoinRound); + CGContextSetLineJoin(currentLayer()->context(), kCGLineJoinRound); break; } case lottie::LineJoin::Bevel: { - CGContextSetLineJoin(_context, kCGLineJoinBevel); + CGContextSetLineJoin(currentLayer()->context(), kCGLineJoinBevel); break; } default: { - CGContextSetLineJoin(_context, kCGLineJoinBevel); + CGContextSetLineJoin(currentLayer()->context(), kCGLineJoinBevel); break; } } switch (lineCap) { case lottie::LineCap::Butt: { - CGContextSetLineCap(_context, kCGLineCapButt); + CGContextSetLineCap(currentLayer()->context(), kCGLineCapButt); break; } case lottie::LineCap::Round: { - CGContextSetLineCap(_context, kCGLineCapRound); + CGContextSetLineCap(currentLayer()->context(), kCGLineCapRound); break; } case lottie::LineCap::Square: { - CGContextSetLineCap(_context, kCGLineCapSquare); + CGContextSetLineCap(currentLayer()->context(), kCGLineCapSquare); break; } default: { - CGContextSetLineCap(_context, kCGLineCapSquare); + CGContextSetLineCap(currentLayer()->context(), kCGLineCapSquare); break; } } @@ -359,11 +381,11 @@ bool addEnumeratedPath(CGContextRef context, CanvasPathEnumerator const &enumera for (const auto value : dashPattern) { mappedDashPattern.push_back(value); } - CGContextSetLineDash(_context, dashPhase, mappedDashPattern.data(), mappedDashPattern.size()); + CGContextSetLineDash(currentLayer()->context(), dashPhase, mappedDashPattern.data(), mappedDashPattern.size()); } - CGContextReplacePathWithStrokedPath(_context); - CGContextClip(_context); + CGContextReplacePathWithStrokedPath(currentLayer()->context()); + CGContextClip(currentLayer()->context()); std::vector components; components.reserve(gradient.colors().size() + 4); @@ -382,59 +404,59 @@ bool addEnumeratedPath(CGContextRef context, CanvasPathEnumerator const &enumera locations.push_back(location); } - CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(_topContext), components.data(), locations.data(), locations.size()); + CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(currentLayer()->context()), components.data(), locations.data(), locations.size()); if (nativeGradient) { - CGContextDrawLinearGradient(_context, nativeGradient, CGPointMake(start.x, start.y), CGPointMake(end.x, end.y), kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); + CGContextDrawLinearGradient(currentLayer()->context(), nativeGradient, CGPointMake(start.x, start.y), CGPointMake(end.x, end.y), kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); CFRelease(nativeGradient); } - CGContextResetClip(_context); - CGContextRestoreGState(_context); + CGContextResetClip(currentLayer()->context()); + CGContextRestoreGState(currentLayer()->context()); } -void CanvasImpl::radialGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) { - CGContextSaveGState(_context); - if (!addEnumeratedPath(_context, enumeratePath)) { - CGContextRestoreGState(_context); +void CoreGraphicsCanvasImpl::radialGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) { + CGContextSaveGState(currentLayer()->context()); + if (!addEnumeratedPath(currentLayer()->context(), enumeratePath)) { + CGContextRestoreGState(currentLayer()->context()); return; } - CGContextSetLineWidth(_context, lineWidth); + CGContextSetLineWidth(currentLayer()->context(), lineWidth); switch (lineJoin) { case lottie::LineJoin::Miter: { - CGContextSetLineJoin(_context, kCGLineJoinMiter); + CGContextSetLineJoin(currentLayer()->context(), kCGLineJoinMiter); break; } case lottie::LineJoin::Round: { - CGContextSetLineJoin(_context, kCGLineJoinRound); + CGContextSetLineJoin(currentLayer()->context(), kCGLineJoinRound); break; } case lottie::LineJoin::Bevel: { - CGContextSetLineJoin(_context, kCGLineJoinBevel); + CGContextSetLineJoin(currentLayer()->context(), kCGLineJoinBevel); break; } default: { - CGContextSetLineJoin(_context, kCGLineJoinBevel); + CGContextSetLineJoin(currentLayer()->context(), kCGLineJoinBevel); break; } } switch (lineCap) { case lottie::LineCap::Butt: { - CGContextSetLineCap(_context, kCGLineCapButt); + CGContextSetLineCap(currentLayer()->context(), kCGLineCapButt); break; } case lottie::LineCap::Round: { - CGContextSetLineCap(_context, kCGLineCapRound); + CGContextSetLineCap(currentLayer()->context(), kCGLineCapRound); break; } case lottie::LineCap::Square: { - CGContextSetLineCap(_context, kCGLineCapSquare); + CGContextSetLineCap(currentLayer()->context(), kCGLineCapSquare); break; } default: { - CGContextSetLineCap(_context, kCGLineCapSquare); + CGContextSetLineCap(currentLayer()->context(), kCGLineCapSquare); break; } } @@ -444,11 +466,11 @@ bool addEnumeratedPath(CGContextRef context, CanvasPathEnumerator const &enumera for (const auto value : dashPattern) { mappedDashPattern.push_back(value); } - CGContextSetLineDash(_context, dashPhase, mappedDashPattern.data(), mappedDashPattern.size()); + CGContextSetLineDash(currentLayer()->context(), dashPhase, mappedDashPattern.data(), mappedDashPattern.size()); } - CGContextReplacePathWithStrokedPath(_context); - CGContextClip(_context); + CGContextReplacePathWithStrokedPath(currentLayer()->context()); + CGContextClip(currentLayer()->context()); std::vector components; components.reserve(gradient.colors().size() + 4); @@ -467,72 +489,116 @@ bool addEnumeratedPath(CGContextRef context, CanvasPathEnumerator const &enumera locations.push_back(location); } - CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(_topContext), components.data(), locations.data(), locations.size()); + CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(currentLayer()->context()), components.data(), locations.data(), locations.size()); if (nativeGradient) { - CGContextDrawRadialGradient(_context, nativeGradient, CGPointMake(startCenter.x, startCenter.y), startRadius, CGPointMake(endCenter.x, endCenter.y), endRadius, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); + CGContextDrawRadialGradient(currentLayer()->context(), nativeGradient, CGPointMake(startCenter.x, startCenter.y), startRadius, CGPointMake(endCenter.x, endCenter.y), endRadius, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); CFRelease(nativeGradient); } - CGContextResetClip(_context); - CGContextRestoreGState(_context); + CGContextResetClip(currentLayer()->context()); + CGContextRestoreGState(currentLayer()->context()); } -void CanvasImpl::fill(lottie::CGRect const &rect, lottie::Color const &fillColor) { - CGFloat components[4] = { fillColor.r, fillColor.g, fillColor.b, fillColor.a }; - CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(_topContext), components); - CGContextSetFillColorWithColor(_context, nativeColor); - CFRelease(nativeColor); - - CGContextFillRect(_context, CGRectMake(rect.x, rect.y, rect.width, rect.height)); +void CoreGraphicsCanvasImpl::clip(CGRect const &rect) { + CGContextClipToRect(currentLayer()->context(), CGRectMake(rect.x, rect.y, rect.width, rect.height)); } -void CanvasImpl::setBlendMode(BlendMode blendMode) { - ::CGBlendMode nativeMode = kCGBlendModeNormal; - switch (blendMode) { - case BlendMode::Normal: { - nativeMode = kCGBlendModeNormal; - break; - } - case BlendMode::DestinationIn: { - nativeMode = kCGBlendModeDestinationIn; +bool CoreGraphicsCanvasImpl::clipPath(CanvasPathEnumerator const &enumeratePath, FillRule fillRule, Transform2D const &transform) { + CGContextSaveGState(currentLayer()->context()); + concatenate(transform); + + if (!addEnumeratedPath(currentLayer()->context(), enumeratePath)) { + CGContextRestoreGState(currentLayer()->context()); + return false; + } + CGContextRestoreGState(currentLayer()->context()); + switch (fillRule) { + case lottie::FillRule::EvenOdd: { + CGContextEOClip(currentLayer()->context()); break; } - case BlendMode::DestinationOut: { - nativeMode = kCGBlendModeDestinationOut; + default: { + CGContextClip(currentLayer()->context()); break; } } - CGContextSetBlendMode(_context, nativeMode); + + return true; } -void CanvasImpl::setAlpha(float alpha) { - CGContextSetAlpha(_context, alpha); +void CoreGraphicsCanvasImpl::concatenate(lottie::Transform2D const &transform) { + CGContextConcatCTM(currentLayer()->context(), CATransform3DGetAffineTransform(nativeTransform(transform))); } -void CanvasImpl::concatenate(lottie::Transform2D const &transform) { - CGContextConcatCTM(_context, CATransform3DGetAffineTransform(nativeTransform(transform))); +std::shared_ptr CoreGraphicsCanvasImpl::makeImage() { + return currentLayer()->makeImage(); } -std::shared_ptr CanvasImpl::makeImage() const { - ::CGImageRef nativeImage = CGBitmapContextCreateImage(_context); - if (nativeImage) { - auto image = std::make_shared(nativeImage); - CFRelease(nativeImage); - return image; +bool CoreGraphicsCanvasImpl::pushLayer(CGRect const &rect, float alpha, std::optional maskMode) { + auto currentTransform = fromNativeTransform(CATransform3DMakeAffineTransform(CGContextGetCTM(currentLayer()->context()))); + + CGRect globalRect(0.0f, 0.0f, 0.0f, 0.0f); + if (rect == CGRect::veryLarge()) { + globalRect = CGRect(0.0f, 0.0f, (float)_width, (float)_height); } else { - return nil; + CGRect transformedRect = rect.applyingTransform(currentTransform); + + CGRect integralTransformedRect( + std::floor(transformedRect.x), + std::floor(transformedRect.y), + std::ceil(transformedRect.width + transformedRect.x - floor(transformedRect.x)), + std::ceil(transformedRect.height + transformedRect.y - floor(transformedRect.y)) + ); + globalRect = integralTransformedRect.intersection(CGRect(0.0, 0.0, (CGFloat)_width, (CGFloat)_height)); } + if (globalRect.width <= 0.0f || globalRect.height <= 0.0f) { + return false; + } + + _layerStack.push_back(std::make_shared(globalRect.width, globalRect.height, Layer::Composition(globalRect, alpha, currentTransform, maskMode))); + concatenate(Transform2D::identity().translated(Vector2D(-globalRect.x, -globalRect.y))); + concatenate(currentTransform); + + return true; } -void CanvasImpl::draw(std::shared_ptr const &other, lottie::CGRect const &rect) { - CanvasImpl *impl = (CanvasImpl *)other.get(); - if (impl->_layer) { - CGContextDrawLayerInRect(_context, CGRectMake(rect.x, rect.y, rect.width, rect.height), impl->_layer); - } else { - auto image = impl->makeImage(); - CGContextDrawImage(_context, CGRectMake(rect.x, rect.y, rect.width, rect.height), ((ImageImpl *)image.get())->nativeImage()); +void CoreGraphicsCanvasImpl::popLayer() { + auto layer = _layerStack[_layerStack.size() - 1]; + _layerStack.pop_back(); + + if (const auto composition = layer->composition()) { + saveState(); + concatenate(composition->transform.inverted()); + + CGContextSetAlpha(currentLayer()->context(), composition->alpha); + + if (composition->maskMode) { + switch (composition->maskMode.value()) { + case Canvas::MaskMode::Normal: { + CGContextSetBlendMode(currentLayer()->context(), kCGBlendModeDestinationIn); + break; + } + case Canvas::MaskMode::Inverse: { + CGContextSetBlendMode(currentLayer()->context(), kCGBlendModeDestinationOut); + break; + } + default: { + break; + } + } + } + + auto image = layer->makeImage(); + CGContextDrawImage(currentLayer()->context(), CGRectMake(composition->rect.x, composition->rect.y, composition->rect.width, composition->rect.height), ((CoreGraphicsCanvasImpl::Image *)image.get())->nativeImage()); + CGContextSetAlpha(currentLayer()->context(), 1.0); + CGContextSetBlendMode(currentLayer()->context(), kCGBlendModeNormal); + + restoreState(); } } +std::shared_ptr &CoreGraphicsCanvasImpl::currentLayer() { + return _layerStack[_layerStack.size() - 1]; } +} diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/NullCanvasImpl.mm b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/NullCanvasImpl.mm deleted file mode 100644 index 60302011802..00000000000 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/NullCanvasImpl.mm +++ /dev/null @@ -1,81 +0,0 @@ -#include "NullCanvasImpl.h" - -namespace lottieRendering { - -namespace { - -void addEnumeratedPath(CanvasPathEnumerator const &enumeratePath) { - enumeratePath([&](PathCommand const &command) { - }); -} - -} - -NullCanvasImpl::NullCanvasImpl(int width, int height) : -_width(width), _height(height), _transform(lottie::Transform2D::identity()) { -} - -NullCanvasImpl::~NullCanvasImpl() { -} - -int NullCanvasImpl::width() const { - return _width; -} - -int NullCanvasImpl::height() const { - return _height; -} - -std::shared_ptr NullCanvasImpl::makeLayer(int width, int height) { - return std::make_shared(width, height); -} - -void NullCanvasImpl::saveState() { -} - -void NullCanvasImpl::restoreState() { -} - -void NullCanvasImpl::fillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Color const &color) { - addEnumeratedPath(enumeratePath); -} - -void NullCanvasImpl::linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) { - addEnumeratedPath(enumeratePath); -} - -void NullCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) { - addEnumeratedPath(enumeratePath); -} - -void NullCanvasImpl::strokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, lottie::Color const &color) { - addEnumeratedPath(enumeratePath); -} - -void NullCanvasImpl::linearGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) { - addEnumeratedPath(enumeratePath); -} - -void NullCanvasImpl::radialGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) { - addEnumeratedPath(enumeratePath); -} - -void NullCanvasImpl::fill(lottie::CGRect const &rect, lottie::Color const &fillColor) { -} - -void NullCanvasImpl::setBlendMode(BlendMode blendMode) { -} - -void NullCanvasImpl::setAlpha(float alpha) { -} - -void NullCanvasImpl::concatenate(lottie::Transform2D const &transform) { -} - -void NullCanvasImpl::draw(std::shared_ptr const &other, lottie::CGRect const &rect) { -} - -void NullCanvasImpl::flush() { -} - -} diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SkiaCanvasImpl.cpp b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SkiaCanvasImpl.cpp new file mode 100644 index 00000000000..48f02e6b16b --- /dev/null +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SkiaCanvasImpl.cpp @@ -0,0 +1,308 @@ +#include "SkiaCanvasImpl.h" + +#include "include/core/SkCanvas.h" +#include "include/core/SkColor.h" +#include "include/core/SkFont.h" +#include "include/core/SkFontTypes.h" +#include "include/core/SkGraphics.h" +#include "include/core/SkPaint.h" +#include "include/core/SkPoint.h" +#include "include/core/SkRect.h" +#include "include/core/SkShader.h" +#include "include/core/SkString.h" +#include "include/core/SkSurface.h" +#include "include/core/SkTileMode.h" +#include "include/core/SkPath.h" +#include "include/core/SkPathEffect.h" +#include "include/effects/SkDashPathEffect.h" +#include "include/effects/SkGradientShader.h" + +#include + +namespace lottie { + +namespace { + +SkColor skColor(Color const &color) { + return SkColorSetARGB((uint8_t)(color.a * 255.0), (uint8_t)(color.r * 255.0), (uint8_t)(color.g * 255.0), (uint8_t)(color.b * 255.0)); +} + +void skPath(CanvasPathEnumerator const &enumeratePath, SkPath &nativePath) { + enumeratePath([&](PathCommand const &command) { + switch (command.type) { + case PathCommandType::MoveTo: { + nativePath.moveTo(command.points[0].x, command.points[0].y); + break; + } + case PathCommandType::LineTo: { + nativePath.lineTo(command.points[0].x, command.points[0].y); + break; + } + case PathCommandType::CurveTo: { + nativePath.cubicTo(command.points[0].x, command.points[0].y, command.points[1].x, command.points[1].y, command.points[2].x, command.points[2].y); + break; + } + case PathCommandType::Close: { + nativePath.close(); + break; + } + } + }); +} + +SkMatrix skMatrix(Transform2D const &transform) { + SkScalar m9[9] = { + transform.rows().columns[0][0], transform.rows().columns[1][0], transform.rows().columns[2][0], + transform.rows().columns[0][1], transform.rows().columns[1][1], transform.rows().columns[2][1], + transform.rows().columns[0][2], transform.rows().columns[1][2], transform.rows().columns[2][2] + }; + SkMatrix matrix; + matrix.set9(m9); + return matrix; +} + +} + +SkiaCanvasImpl::SkiaCanvasImpl(int width, int height) { + int bytesPerRow = width * 4; + _pixelData = malloc(bytesPerRow * height); + _ownsPixelData = true; + + _surface = SkSurfaces::WrapPixels( + SkImageInfo::MakeN32Premul(width, height), + _pixelData, + bytesPerRow, + nullptr + ); + + _canvas = _surface->getCanvas(); + _canvas->resetMatrix(); + _canvas->clear(SkColorSetARGB(0, 0, 0, 0)); +} + +SkiaCanvasImpl::SkiaCanvasImpl(int width, int height, int bytesPerRow, void *pixelData) { + _pixelData = pixelData; + _ownsPixelData = false; + + _surface = SkSurfaces::WrapPixels( + SkImageInfo::MakeN32Premul(width, height), + _pixelData, + bytesPerRow, + nullptr + ); + + _canvas = _surface->getCanvas(); + _canvas->resetMatrix(); + _canvas->clear(SkColorSetARGB(0, 0, 0, 0)); +} + +SkiaCanvasImpl::~SkiaCanvasImpl() { + if (_ownsPixelData) { + free(_pixelData); + } +} + +void SkiaCanvasImpl::saveState() { + _canvas->save(); +} + +void SkiaCanvasImpl::restoreState() { + _canvas->restore(); +} + +void SkiaCanvasImpl::fillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Color const &color) { + SkPaint paint; + paint.setColor(skColor(color)); + paint.setAntiAlias(true); + + SkPath nativePath; + skPath(enumeratePath, nativePath); + nativePath.setFillType(fillRule == FillRule::EvenOdd ? SkPathFillType::kEvenOdd : SkPathFillType::kWinding); + + _canvas->drawPath(nativePath, paint); +} + +void SkiaCanvasImpl::linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) { + SkPaint paint; + paint.setAntiAlias(true); + paint.setDither(false); + paint.setStyle(SkPaint::Style::kFill_Style); + + SkPoint linearPoints[2] = { + SkPoint::Make(start.x, start.y), + SkPoint::Make(end.x, end.y) + }; + + std::vector colors; + for (const auto &color : gradient.colors()) { + colors.push_back(skColor(Color(color.r, color.g, color.b, color.a))); + } + + std::vector locations; + for (auto location : gradient.locations()) { + locations.push_back(location); + } + + paint.setShader(SkGradientShader::MakeLinear(linearPoints, colors.data(), locations.data(), (int)colors.size(), SkTileMode::kClamp)); + + SkPath nativePath; + skPath(enumeratePath, nativePath); + nativePath.setFillType(fillRule == FillRule::EvenOdd ? SkPathFillType::kEvenOdd : SkPathFillType::kWinding); + + _canvas->drawPath(nativePath, paint); +} + +void SkiaCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, Vector2D const ¢er, float radius) { + SkPaint paint; + paint.setAntiAlias(true); + paint.setStyle(SkPaint::Style::kFill_Style); + + std::vector colors; + for (const auto &color : gradient.colors()) { + colors.push_back(skColor(Color(color.r, color.g, color.b, color.a))); + } + + std::vector locations; + for (auto location : gradient.locations()) { + locations.push_back(location); + } + + paint.setShader(SkGradientShader::MakeRadial(SkPoint::Make(center.x, center.y), radius, colors.data(), locations.data(), (int)colors.size(), SkTileMode::kClamp)); + + SkPath nativePath; + skPath(enumeratePath, nativePath); + nativePath.setFillType(fillRule == FillRule::EvenOdd ? SkPathFillType::kEvenOdd : SkPathFillType::kWinding); + + _canvas->drawPath(nativePath, paint); +} + +void SkiaCanvasImpl::strokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, lottie::Color const &color) { + if (lineWidth <= FLT_EPSILON) { + return; + } + SkPaint paint; + paint.setAntiAlias(true); + paint.setColor(skColor(color)); + paint.setStyle(SkPaint::Style::kStroke_Style); + + paint.setStrokeWidth(lineWidth); + switch (lineJoin) { + case LineJoin::Miter: { + paint.setStrokeJoin(SkPaint::Join::kMiter_Join); + break; + } + case LineJoin::Round: { + paint.setStrokeJoin(SkPaint::Join::kRound_Join); + break; + } + case LineJoin::Bevel: { + paint.setStrokeJoin(SkPaint::Join::kBevel_Join); + break; + } + default: { + paint.setStrokeJoin(SkPaint::Join::kBevel_Join); + break; + } + } + + switch (lineCap) { + case LineCap::Butt: { + paint.setStrokeCap(SkPaint::Cap::kButt_Cap); + break; + } + case LineCap::Round: { + paint.setStrokeCap(SkPaint::Cap::kRound_Cap); + break; + } + case LineCap::Square: { + paint.setStrokeCap(SkPaint::Cap::kSquare_Cap); + break; + } + default: { + paint.setStrokeCap(SkPaint::Cap::kSquare_Cap); + break; + } + } + + if (!dashPattern.empty()) { + std::vector intervals; + intervals.reserve(dashPattern.size()); + for (auto value : dashPattern) { + intervals.push_back(value); + } + if (intervals.size() == 1) { + intervals.push_back(intervals[0]); + } + paint.setPathEffect(SkDashPathEffect::Make(intervals.data(), (int)intervals.size(), dashPhase)); + } + + SkPath nativePath; + skPath(enumeratePath, nativePath); + + _canvas->drawPath(nativePath, paint); +} + +void SkiaCanvasImpl::linearGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) { + assert(false); +} + +void SkiaCanvasImpl::radialGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) { + assert(false); +} + +void SkiaCanvasImpl::clip(CGRect const &rect) { + _canvas->clipRect(SkRect::MakeXYWH(rect.x, rect.y, rect.width, rect.height), true); +} + +bool SkiaCanvasImpl::clipPath(CanvasPathEnumerator const &enumeratePath, FillRule fillRule, Transform2D const &transform) { + SkPath nativePath; + skPath(enumeratePath, nativePath); + nativePath.setFillType(fillRule == FillRule::EvenOdd ? SkPathFillType::kEvenOdd : SkPathFillType::kWinding); + if (!transform.isIdentity()) { + nativePath.transform(skMatrix(transform)); + } + _canvas->clipPath(nativePath, true); + + return true; +} + +void SkiaCanvasImpl::concatenate(lottie::Transform2D const &transform) { + _canvas->concat(skMatrix(transform)); +} + +bool SkiaCanvasImpl::pushLayer(CGRect const &rect, float alpha, std::optional maskMode) { + SkPaint paint; + paint.setAntiAlias(true); + paint.setAlphaf(alpha); + if (maskMode) { + switch (maskMode.value()) { + case Canvas::MaskMode::Normal: { + paint.setBlendMode(SkBlendMode::kDstIn); + break; + } + case Canvas::MaskMode::Inverse: { + paint.setBlendMode(SkBlendMode::kDstOut); + break; + } + default: { + break; + } + } + } + + _canvas->saveLayer(SkRect::MakeXYWH(rect.x, rect.y, rect.width, rect.height), &paint); + return true; +} + +void SkiaCanvasImpl::popLayer() { + _canvas->restore(); +} + +void SkiaCanvasImpl::flush() { +} + +sk_sp SkiaCanvasImpl::surface() const { + return _surface; +} + +} diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/NullCanvasImpl.h b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SkiaCanvasImpl.h similarity index 55% rename from Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/NullCanvasImpl.h rename to Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SkiaCanvasImpl.h index 56918703ae0..59c0b4f48e5 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/NullCanvasImpl.h +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SkiaCanvasImpl.h @@ -1,45 +1,44 @@ -#ifndef NullCanvasImpl_h -#define NullCanvasImpl_h +#ifndef SkiaCanvasImpl_h +#define SkiaCanvasImpl_h -#include "Canvas.h" +#include -namespace lottieRendering { +#include "include/core/SkCanvas.h" +#include "include/core/SkSurface.h" -class NullCanvasImpl: public Canvas { +namespace lottie { + +class SkiaCanvasImpl: public Canvas { public: - NullCanvasImpl(int width, int height); - virtual ~NullCanvasImpl(); - - virtual int width() const override; - virtual int height() const override; - - virtual std::shared_ptr makeLayer(int width, int height) override; + SkiaCanvasImpl(int width, int height); + SkiaCanvasImpl(int width, int height, int bytesPerRow, void *pixelData); + virtual ~SkiaCanvasImpl(); virtual void saveState() override; virtual void restoreState() override; virtual void fillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Color const &color) override; - virtual void linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottieRendering::Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override; - virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottieRendering::Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override; + virtual void linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override; + virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, Vector2D const ¢er, float radius) override; virtual void strokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, lottie::Color const &color) override; virtual void linearGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override; virtual void radialGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override; - virtual void fill(lottie::CGRect const &rect, lottie::Color const &fillColor) override; - - virtual void setBlendMode(BlendMode blendMode) override; - - virtual void setAlpha(float alpha) override; + virtual void clip(CGRect const &rect) override; + virtual bool clipPath(CanvasPathEnumerator const &enumeratePath, FillRule fillRule, Transform2D const &transform) override; virtual void concatenate(lottie::Transform2D const &transform) override; - virtual void draw(std::shared_ptr const &other, lottie::CGRect const &rect) override; + virtual bool pushLayer(CGRect const &rect, float alpha, std::optional maskMode) override; + virtual void popLayer() override; void flush(); - + sk_sp surface() const; + private: - float _width = 0.0f; - float _height = 0.0f; - lottie::Transform2D _transform; + void *_pixelData = nullptr; + bool _ownsPixelData = false; + sk_sp _surface; + SkCanvas *_canvas = nullptr; }; } diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm index c71328ef452..afa87543d52 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm @@ -1,631 +1,125 @@ #import -#import "Canvas.h" +#import +#import + #import "CoreGraphicsCanvasImpl.h" -#import "ThorVGCanvasImpl.h" -#import "NullCanvasImpl.h" +#import "SkiaCanvasImpl.h" #include +#include +#include -namespace { - -static constexpr float minVisibleAlpha = 0.5f / 255.0f; - -static constexpr float minGlobalRectCalculationSize = 200.0f; - -struct TransformedPath { - lottie::BezierPath path; - lottie::Transform2D transform; - - TransformedPath(lottie::BezierPath const &path_, lottie::Transform2D const &transform_) : - path(path_), - transform(transform_) { - } -}; - -static lottie::CGRect collectPathBoundingBoxes(std::shared_ptr item, size_t subItemLimit, lottie::Transform2D const &parentTransform, bool skipApplyTransform, lottie::BezierPathsBoundingBoxContext &bezierPathsBoundingBoxContext) { - //TODO:remove skipApplyTransform - lottie::Transform2D effectiveTransform = parentTransform; - if (!skipApplyTransform && item->isGroup) { - effectiveTransform = item->transform * effectiveTransform; - } - - size_t maxSubitem = std::min(item->subItems.size(), subItemLimit); - - lottie::CGRect boundingBox(0.0, 0.0, 0.0, 0.0); - if (item->path) { - if (item->path->needsBoundsRecalculation) { - item->path->bounds = lottie::bezierPathsBoundingBoxParallel(bezierPathsBoundingBoxContext, item->path->path); - item->path->needsBoundsRecalculation = false; - } - boundingBox = item->path->bounds.applyingTransform(effectiveTransform); - } - - for (size_t i = 0; i < maxSubitem; i++) { - auto &subItem = item->subItems[i]; - - lottie::CGRect subItemBoundingBox = collectPathBoundingBoxes(subItem, INT32_MAX, effectiveTransform, false, bezierPathsBoundingBoxContext); - - if (boundingBox.empty()) { - boundingBox = subItemBoundingBox; - } else { - boundingBox = boundingBox.unionWith(subItemBoundingBox); - } - } - - return boundingBox; -} - -static void enumeratePaths(std::shared_ptr item, size_t subItemLimit, lottie::Transform2D const &parentTransform, bool skipApplyTransform, std::function const &onPath) { - //TODO:remove skipApplyTransform - lottie::Transform2D effectiveTransform = parentTransform; - if (!skipApplyTransform && item->isGroup) { - effectiveTransform = item->transform * effectiveTransform; - } - - size_t maxSubitem = std::min(item->subItems.size(), subItemLimit); - - if (item->path) { - onPath(item->path->path, effectiveTransform); - } - - for (size_t i = 0; i < maxSubitem; i++) { - auto &subItem = item->subItems[i]; - - enumeratePaths(subItem, INT32_MAX, effectiveTransform, false, onPath); - } -} - -} - -namespace lottie { - -static std::optional getRenderContentItemGlobalRect(std::shared_ptr const &contentItem, lottie::Vector2D const &globalSize, lottie::Transform2D const &parentTransform, BezierPathsBoundingBoxContext &bezierPathsBoundingBoxContext) { - auto currentTransform = parentTransform; - Transform2D localTransform = contentItem->transform; - currentTransform = localTransform * currentTransform; - - std::optional globalRect; - for (const auto &shadingVariant : contentItem->shadings) { - lottie::CGRect shapeBounds = collectPathBoundingBoxes(contentItem, shadingVariant->subItemLimit, lottie::Transform2D::identity(), true, bezierPathsBoundingBoxContext); - - if (shadingVariant->stroke) { - shapeBounds = shapeBounds.insetBy(-shadingVariant->stroke->lineWidth / 2.0, -shadingVariant->stroke->lineWidth / 2.0); - } else if (shadingVariant->fill) { - } else { - continue; - } - - CGRect shapeGlobalBounds = shapeBounds.applyingTransform(currentTransform); - if (globalRect) { - globalRect = globalRect->unionWith(shapeGlobalBounds); - } else { - globalRect = shapeGlobalBounds; - } - } - - for (const auto &subItem : contentItem->subItems) { - auto subGlobalRect = getRenderContentItemGlobalRect(subItem, globalSize, currentTransform, bezierPathsBoundingBoxContext); - if (subGlobalRect) { - if (globalRect) { - globalRect = globalRect->unionWith(subGlobalRect.value()); - } else { - globalRect = subGlobalRect.value(); - } - } - } - - if (globalRect) { - CGRect integralGlobalRect( - std::floor(globalRect->x), - std::floor(globalRect->y), - std::ceil(globalRect->width + globalRect->x - floor(globalRect->x)), - std::ceil(globalRect->height + globalRect->y - floor(globalRect->y)) - ); - return integralGlobalRect.intersection(CGRect(0.0, 0.0, globalSize.x, globalSize.y)); - } else { - return std::nullopt; - } -} +#import -static std::optional getRenderNodeGlobalRect(std::shared_ptr const &node, lottie::Vector2D const &globalSize, lottie::Transform2D const &parentTransform, bool isInvertedMatte, BezierPathsBoundingBoxContext &bezierPathsBoundingBoxContext) { - if (node->isHidden() || node->alpha() < minVisibleAlpha) { - return std::nullopt; - } - - auto currentTransform = parentTransform; - Transform2D localTransform = node->transform(); - currentTransform = localTransform * currentTransform; - - std::optional globalRect; - if (node->_contentItem) { - globalRect = getRenderContentItemGlobalRect(node->_contentItem, globalSize, currentTransform, bezierPathsBoundingBoxContext); - } - - if (isInvertedMatte) { - CGRect globalBounds = CGRect(0.0f, 0.0f, node->size().x, node->size().y).applyingTransform(currentTransform); - if (globalRect) { - globalRect = globalRect->unionWith(globalBounds); - } else { - globalRect = globalBounds; - } - } - - for (const auto &subNode : node->subnodes()) { - auto subGlobalRect = getRenderNodeGlobalRect(subNode, globalSize, currentTransform, false, bezierPathsBoundingBoxContext); - if (subGlobalRect) { - if (globalRect) { - globalRect = globalRect->unionWith(subGlobalRect.value()); - } else { - globalRect = subGlobalRect.value(); - } - } - } - - if (globalRect) { - CGRect integralGlobalRect( - std::floor(globalRect->x), - std::floor(globalRect->y), - std::ceil(globalRect->width + globalRect->x - floor(globalRect->x)), - std::ceil(globalRect->height + globalRect->y - floor(globalRect->y)) - ); - return integralGlobalRect.intersection(CGRect(0.0, 0.0, globalSize.x, globalSize.y)); - } else { - return std::nullopt; - } +CGRect getPathNativeBoundingBox(CGPathRef _Nonnull path) { + auto rect = calculatePathBoundingBox(path); + return CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height); } +@interface SoftwareLottieRenderer() { + std::shared_ptr _renderer; + std::shared_ptr _canvasRenderer; } -namespace { +@end -static void drawLottieContentItem(std::shared_ptr const &parentContext, std::shared_ptr item, float parentAlpha, lottie::Vector2D const &globalSize, lottie::Transform2D const &parentTransform, lottie::BezierPathsBoundingBoxContext &bezierPathsBoundingBoxContext) { - auto currentTransform = parentTransform; - lottie::Transform2D localTransform = item->transform; - currentTransform = localTransform * currentTransform; - - float normalizedOpacity = item->alpha; - float layerAlpha = ((float)normalizedOpacity) * parentAlpha; - - if (normalizedOpacity == 0.0f) { - return; - } - - parentContext->saveState(); - - std::shared_ptr const *currentContext; - std::shared_ptr tempContext; - - bool needsTempContext = false; - needsTempContext = layerAlpha != 1.0 && item->drawContentCount > 1; - - std::optional globalRect; - if (needsTempContext) { - if (globalSize.x <= minGlobalRectCalculationSize && globalSize.y <= minGlobalRectCalculationSize) { - globalRect = lottie::CGRect(0.0, 0.0, globalSize.x, globalSize.y); - } else { - globalRect = lottie::getRenderContentItemGlobalRect(item, globalSize, parentTransform, bezierPathsBoundingBoxContext); - } - if (!globalRect || globalRect->width <= 0.0f || globalRect->height <= 0.0f) { - parentContext->restoreState(); - return; - } - - auto tempContextValue = parentContext->makeLayer((int)(globalRect->width), (int)(globalRect->height)); - tempContext = tempContextValue; - - currentContext = &tempContext; - (*currentContext)->concatenate(lottie::Transform2D::identity().translated(lottie::Vector2D(-globalRect->x, -globalRect->y))); - - (*currentContext)->saveState(); - (*currentContext)->concatenate(currentTransform); - } else { - currentContext = &parentContext; - } - - parentContext->concatenate(item->transform); - - float renderAlpha = 1.0; - if (tempContext) { - renderAlpha = 1.0; - } else { - renderAlpha = layerAlpha; - } - - for (const auto &shading : item->shadings) { - lottieRendering::CanvasPathEnumerator iteratePaths; - if (shading->explicitPath) { - auto itemPaths = shading->explicitPath.value(); - iteratePaths = [itemPaths = itemPaths](std::function iterate) -> void { - lottieRendering::PathCommand pathCommand; - for (const auto &path : itemPaths) { - std::optional previousElement; - for (const auto &element : path.elements()) { - if (previousElement.has_value()) { - if (previousElement->vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) { - pathCommand.type = lottieRendering::PathCommandType::LineTo; - pathCommand.points[0] = CGPointMake(element.vertex.point.x, element.vertex.point.y); - iterate(pathCommand); - } else { - pathCommand.type = lottieRendering::PathCommandType::CurveTo; - pathCommand.points[2] = CGPointMake(element.vertex.point.x, element.vertex.point.y); - pathCommand.points[1] = CGPointMake(element.vertex.inTangent.x, element.vertex.inTangent.y); - pathCommand.points[0] = CGPointMake(previousElement->vertex.outTangent.x, previousElement->vertex.outTangent.y); - iterate(pathCommand); - } - } else { - pathCommand.type = lottieRendering::PathCommandType::MoveTo; - pathCommand.points[0] = CGPointMake(element.vertex.point.x, element.vertex.point.y); - iterate(pathCommand); - } - previousElement = element; - } - if (path.closed().value_or(true)) { - pathCommand.type = lottieRendering::PathCommandType::Close; - iterate(pathCommand); - } - } - }; - } else { - iteratePaths = [&](std::function iterate) { - enumeratePaths(item, shading->subItemLimit, lottie::Transform2D::identity(), true, [&](lottie::BezierPath const &sourcePath, lottie::Transform2D const &transform) { - auto path = sourcePath.copyUsingTransform(transform); - - lottieRendering::PathCommand pathCommand; - std::optional previousElement; - for (const auto &element : path.elements()) { - if (previousElement.has_value()) { - if (previousElement->vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) { - pathCommand.type = lottieRendering::PathCommandType::LineTo; - pathCommand.points[0] = CGPointMake(element.vertex.point.x, element.vertex.point.y); - iterate(pathCommand); - } else { - pathCommand.type = lottieRendering::PathCommandType::CurveTo; - pathCommand.points[2] = CGPointMake(element.vertex.point.x, element.vertex.point.y); - pathCommand.points[1] = CGPointMake(element.vertex.inTangent.x, element.vertex.inTangent.y); - pathCommand.points[0] = CGPointMake(previousElement->vertex.outTangent.x, previousElement->vertex.outTangent.y); - iterate(pathCommand); - } - } else { - pathCommand.type = lottieRendering::PathCommandType::MoveTo; - pathCommand.points[0] = CGPointMake(element.vertex.point.x, element.vertex.point.y); - iterate(pathCommand); - } - previousElement = element; - } - if (path.closed().value_or(true)) { - pathCommand.type = lottieRendering::PathCommandType::Close; - iterate(pathCommand); - } - }); - }; - } - - /*auto iteratePaths = [&](std::function iterate) -> void { - lottieRendering::PathCommand pathCommand; - for (const auto &path : itemPaths) { - std::optional previousElement; - for (const auto &element : path.elements()) { - if (previousElement.has_value()) { - if (previousElement->vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) { - pathCommand.type = lottieRendering::PathCommandType::LineTo; - pathCommand.points[0] = CGPointMake(element.vertex.point.x, element.vertex.point.y); - iterate(pathCommand); - } else { - pathCommand.type = lottieRendering::PathCommandType::CurveTo; - pathCommand.points[2] = CGPointMake(element.vertex.point.x, element.vertex.point.y); - pathCommand.points[1] = CGPointMake(element.vertex.inTangent.x, element.vertex.inTangent.y); - pathCommand.points[0] = CGPointMake(previousElement->vertex.outTangent.x, previousElement->vertex.outTangent.y); - iterate(pathCommand); - } - } else { - pathCommand.type = lottieRendering::PathCommandType::MoveTo; - pathCommand.points[0] = CGPointMake(element.vertex.point.x, element.vertex.point.y); - iterate(pathCommand); - } - previousElement = element; - } - if (path.closed().value_or(true)) { - pathCommand.type = lottieRendering::PathCommandType::Close; - iterate(pathCommand); - } - } - };*/ - - if (shading->stroke) { - if (shading->stroke->shading->type() == lottie::RenderTreeNodeContentItem::ShadingType::Solid) { - lottie::RenderTreeNodeContentItem::SolidShading *solidShading = (lottie::RenderTreeNodeContentItem::SolidShading *)shading->stroke->shading.get(); - - if (solidShading->opacity != 0.0) { - lottie::LineJoin lineJoin = lottie::LineJoin::Bevel; - switch (shading->stroke->lineJoin) { - case lottie::LineJoin::Bevel: { - lineJoin = lottie::LineJoin::Bevel; - break; - } - case lottie::LineJoin::Round: { - lineJoin = lottie::LineJoin::Round; - break; - } - case lottie::LineJoin::Miter: { - lineJoin = lottie::LineJoin::Miter; - break; - } - default: { - break; - } - } - - lottie::LineCap lineCap = lottie::LineCap::Square; - switch (shading->stroke->lineCap) { - case lottie::LineCap::Butt: { - lineCap = lottie::LineCap::Butt; - break; - } - case lottie::LineCap::Round: { - lineCap = lottie::LineCap::Round; - break; - } - case lottie::LineCap::Square: { - lineCap = lottie::LineCap::Square; - break; - } - default: { - break; - } - } - - std::vector dashPattern; - if (!shading->stroke->dashPattern.empty()) { - dashPattern = shading->stroke->dashPattern; - } - - (*currentContext)->strokePath(iteratePaths, shading->stroke->lineWidth, lineJoin, lineCap, shading->stroke->dashPhase, dashPattern, lottie::Color(solidShading->color.r, solidShading->color.g, solidShading->color.b, solidShading->color.a * solidShading->opacity * renderAlpha)); - } else if (shading->stroke->shading->type() == lottie::RenderTreeNodeContentItem::ShadingType::Gradient) { - //TODO:gradient stroke - } - } - } else if (shading->fill) { - lottie::FillRule rule = lottie::FillRule::NonZeroWinding; - switch (shading->fill->rule) { - case lottie::FillRule::EvenOdd: { - rule = lottie::FillRule::EvenOdd; - break; - } - case lottie::FillRule::NonZeroWinding: { - rule = lottie::FillRule::NonZeroWinding; - break; - } - default: { - break; - } - } - - if (shading->fill->shading->type() == lottie::RenderTreeNodeContentItem::ShadingType::Solid) { - lottie::RenderTreeNodeContentItem::SolidShading *solidShading = (lottie::RenderTreeNodeContentItem::SolidShading *)shading->fill->shading.get(); - if (solidShading->opacity != 0.0) { - (*currentContext)->fillPath(iteratePaths, rule, lottie::Color(solidShading->color.r, solidShading->color.g, solidShading->color.b, solidShading->color.a * solidShading->opacity * renderAlpha)); - } - } else if (shading->fill->shading->type() == lottie::RenderTreeNodeContentItem::ShadingType::Gradient) { - lottie::RenderTreeNodeContentItem::GradientShading *gradientShading = (lottie::RenderTreeNodeContentItem::GradientShading *)shading->fill->shading.get(); - - if (gradientShading->opacity != 0.0) { - std::vector colors; - std::vector locations; - for (const auto &color : gradientShading->colors) { - colors.push_back(lottie::Color(color.r, color.g, color.b, color.a * gradientShading->opacity * renderAlpha)); - } - locations = gradientShading->locations; - - lottieRendering::Gradient gradient(colors, locations); - lottie::Vector2D start(gradientShading->start.x, gradientShading->start.y); - lottie::Vector2D end(gradientShading->end.x, gradientShading->end.y); - - switch (gradientShading->gradientType) { - case lottie::GradientType::Linear: { - (*currentContext)->linearGradientFillPath(iteratePaths, rule, gradient, start, end); - break; - } - case lottie::GradientType::Radial: { - (*currentContext)->radialGradientFillPath(iteratePaths, rule, gradient, start, 0.0, start, start.distanceTo(end)); - break; - } - default: { - break; - } - } - } - } - } - } - - for (auto it = item->subItems.rbegin(); it != item->subItems.rend(); it++) { - const auto &subItem = *it; - drawLottieContentItem(*currentContext, subItem, renderAlpha, globalSize, currentTransform, bezierPathsBoundingBoxContext); - } - - if (tempContext) { - tempContext->restoreState(); - - parentContext->concatenate(currentTransform.inverted()); - parentContext->setAlpha(layerAlpha); - parentContext->draw(tempContext, globalRect.value()); - parentContext->setAlpha(1.0); - } - - parentContext->restoreState(); -} +@implementation SoftwareLottieRenderer -static void renderLottieRenderNode(std::shared_ptr node, std::shared_ptr const &parentContext, lottie::Vector2D const &globalSize, lottie::Transform2D const &parentTransform, float parentAlpha, bool isInvertedMatte, lottie::BezierPathsBoundingBoxContext &bezierPathsBoundingBoxContext) { - float normalizedOpacity = node->alpha(); - float layerAlpha = ((float)normalizedOpacity) * parentAlpha; - - if (node->isHidden() || normalizedOpacity < minVisibleAlpha) { - return; - } - - auto currentTransform = parentTransform; - lottie::Transform2D localTransform = node->transform(); - currentTransform = localTransform * currentTransform; - - std::shared_ptr maskContext; - std::shared_ptr currentContext; - std::shared_ptr tempContext; - - bool masksToBounds = node->masksToBounds(); - if (masksToBounds) { - lottie::CGRect effectiveGlobalBounds = lottie::CGRect(0.0f, 0.0f, node->size().x, node->size().y).applyingTransform(currentTransform); - if (effectiveGlobalBounds.width <= 0.0f || effectiveGlobalBounds.height <= 0.0f) { - return; - } - if (effectiveGlobalBounds.contains(lottie::CGRect(0.0, 0.0, globalSize.x, globalSize.y))) { - masksToBounds = false; - } - } - - parentContext->saveState(); - - bool needsTempContext = false; - if (node->mask() && !node->mask()->isHidden() && node->mask()->alpha() >= minVisibleAlpha) { - needsTempContext = true; - } else { - needsTempContext = layerAlpha != 1.0 || masksToBounds; - } - - std::optional globalRect; - if (needsTempContext) { - if (globalSize.x <= minGlobalRectCalculationSize && globalSize.y <= minGlobalRectCalculationSize) { - globalRect = lottie::CGRect(0.0, 0.0, globalSize.x, globalSize.y); - } else { - globalRect = lottie::getRenderNodeGlobalRect(node, globalSize, parentTransform, false, bezierPathsBoundingBoxContext); - } - if (!globalRect || globalRect->width <= 0.0f || globalRect->height <= 0.0f) { - parentContext->restoreState(); - return; - } - - if ((node->mask() && !node->mask()->isHidden() && node->mask()->alpha() >= minVisibleAlpha) || masksToBounds) { - auto maskBackingStorage = parentContext->makeLayer((int)(globalRect->width), (int)(globalRect->height)); - - maskBackingStorage->concatenate(lottie::Transform2D::identity().translated(lottie::Vector2D(-globalRect->x, -globalRect->y))); - maskBackingStorage->concatenate(currentTransform); - - if (masksToBounds) { - maskBackingStorage->fill(lottie::CGRect(0.0f, 0.0f, node->size().x, node->size().y), lottie::Color(1.0f, 1.0f, 1.0f, 1.0f)); - } - if (node->mask() && !node->mask()->isHidden() && node->mask()->alpha() >= minVisibleAlpha) { - renderLottieRenderNode(node->mask(), maskBackingStorage, globalSize, currentTransform, 1.0, node->invertMask(), bezierPathsBoundingBoxContext); - } - - maskContext = maskBackingStorage; - } - - auto tempContextValue = parentContext->makeLayer((int)(globalRect->width), (int)(globalRect->height)); - tempContext = tempContextValue; - - currentContext = tempContextValue; - currentContext->concatenate(lottie::Transform2D::identity().translated(lottie::Vector2D(-globalRect->x, -globalRect->y))); - - currentContext->saveState(); - currentContext->concatenate(currentTransform); - } else { - currentContext = parentContext; - } - - parentContext->concatenate(node->transform()); - - float renderAlpha = 1.0f; - if (tempContext) { - renderAlpha = 1.0f; - } else { - renderAlpha = layerAlpha; - } - - if (node->_contentItem) { - drawLottieContentItem(currentContext, node->_contentItem, renderAlpha, globalSize, currentTransform, bezierPathsBoundingBoxContext); - } - - if (isInvertedMatte) { - currentContext->fill(lottie::CGRect(0.0f, 0.0f, node->size().x, node->size().y), lottie::Color(0.0f, 0.0f, 0.0f, 1.0f)); - currentContext->setBlendMode(lottieRendering::BlendMode::DestinationOut); - } - - for (const auto &subnode : node->subnodes()) { - renderLottieRenderNode(subnode, currentContext, globalSize, currentTransform, renderAlpha, false, bezierPathsBoundingBoxContext); - } - - if (tempContext) { - tempContext->restoreState(); - - if (maskContext) { - tempContext->setBlendMode(lottieRendering::BlendMode::DestinationIn); - tempContext->draw(maskContext, lottie::CGRect(globalRect->x, globalRect->y, globalRect->width, globalRect->height)); +- (instancetype _Nullable)initWithData:(NSData * _Nonnull)data { + self = [super init]; + if (self != nil) { + _renderer = lottie::Renderer::make(std::string((uint8_t const *)data.bytes, ((uint8_t const *)data.bytes) + data.length)); + if (!_renderer) { + return nil; } - parentContext->concatenate(currentTransform.inverted()); - parentContext->setAlpha(layerAlpha); - parentContext->draw(tempContext, globalRect.value()); + _canvasRenderer = std::make_shared(); } - - parentContext->restoreState(); + return self; } +- (NSInteger)frameCount { + return (NSInteger)_renderer->frameCount(); } -CGRect getPathNativeBoundingBox(CGPathRef _Nonnull path) { - auto rect = calculatePathBoundingBox(path); - return CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height); +- (NSInteger)framesPerSecond { + return (NSInteger)_renderer->framesPerSecond(); } -@interface SoftwareLottieRenderer() { - LottieAnimationContainer *_animationContainer; - std::shared_ptr _bezierPathsBoundingBoxContext; +- (CGSize)size { + lottie::Vector2D size = _renderer->size(); + return CGSizeMake(size.x, size.y); } -@end - -@implementation SoftwareLottieRenderer - -- (instancetype _Nonnull)initWithAnimationContainer:(LottieAnimationContainer * _Nonnull)animationContainer { - self = [super init]; - if (self != nil) { - _animationContainer = animationContainer; - _bezierPathsBoundingBoxContext = std::make_shared(); - } - return self; +- (void)setFrame:(CGFloat)index { + _renderer->setFrame((float)index); } -- (UIImage * _Nullable)renderForSize:(CGSize)size useReferenceRendering:(bool)useReferenceRendering { - LottieAnimation *animation = _animationContainer.animation; - std::shared_ptr renderNode = [_animationContainer internalGetRootRenderTreeNode]; +- (UIImage * _Nullable)renderForSize:(CGSize)size useReferenceRendering:(bool)useReferenceRendering canUseMoreMemory:(bool)canUseMoreMemory skipImageGeneration:(bool)skipImageGeneration { + std::shared_ptr renderNode = _renderer->renderNode(); if (!renderNode) { return nil; } - lottie::Transform2D rootTransform = lottie::Transform2D::identity().scaled(lottie::Vector2D(size.width / (float)animation.size.width, size.height / (float)animation.size.height)); + lottie::CanvasRenderer::Configuration configuration; + configuration.canUseMoreMemory = canUseMoreMemory; + //configuration.canUseMoreMemory = true; + //configuration.disableGroupTransparency = true; if (useReferenceRendering) { - auto context = std::make_shared((int)size.width, (int)size.height); - - CGPoint scale = CGPointMake(size.width / (CGFloat)animation.size.width, size.height / (CGFloat)animation.size.height); - context->concatenate(lottie::Transform2D::makeScale(scale.x, scale.y)); + auto context = std::make_shared((int)size.width, (int)size.height); - renderLottieRenderNode(renderNode, context, lottie::Vector2D(context->width(), context->height()), rootTransform, 1.0, false, *_bezierPathsBoundingBoxContext.get()); + _canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height), configuration); auto image = context->makeImage(); - return [[UIImage alloc] initWithCGImage:std::static_pointer_cast(image)->nativeImage()]; + return [[UIImage alloc] initWithCGImage:std::static_pointer_cast(image)->nativeImage()]; } else { - //auto context = std::make_shared((int)size.width, (int)size.height); - auto context = std::make_shared((int)size.width, (int)size.height); - - CGPoint scale = CGPointMake(size.width / (CGFloat)animation.size.width, size.height / (CGFloat)animation.size.height); - context->concatenate(lottie::Transform2D::makeScale(scale.x, scale.y)); - - //renderLottieRenderNode(renderNode, context, lottie::Vector2D(context->width(), context->height()), rootTransform, 1.0, false, *_bezierPathsBoundingBoxContext.get()); - - return nil; + if ((int64_t)"" > 0) { + int bytesPerRow = ((int)size.width) * 4; + void *pixelData = malloc(bytesPerRow * (int)size.height); + auto context = std::make_shared((int)size.width, (int)size.height, bytesPerRow, pixelData); + + _canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height), configuration); + context->flush(); + + if (skipImageGeneration) { + free(pixelData); + } else { + vImage_Buffer src; + src.data = (void *)pixelData; + src.width = (int)size.width; + src.height = (int)size.height; + src.rowBytes = bytesPerRow; + + uint8_t permuteMap[4] = {2, 1, 0, 3}; + vImagePermuteChannels_ARGB8888(&src, &src, permuteMap, kvImageDoNotTile); + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + + CGContextRef targetContext = CGBitmapContextCreate(pixelData, (int)size.width, (int)size.height, 8, bytesPerRow, colorSpace, bitmapInfo); + CGColorSpaceRelease(colorSpace); + + CGImageRef bitmapImage = CGBitmapContextCreateImage(targetContext); + UIImage *image = [[UIImage alloc] initWithCGImage:bitmapImage scale:1.0f orientation:UIImageOrientationDownMirrored]; + CGImageRelease(bitmapImage); + + CGContextRelease(targetContext); + + free(pixelData); + + return image; + } + } else { + auto context = std::make_shared((int)size.width, (int)size.height); + _canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height), configuration); + + return nil; + } } + return nil; } @end diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.h b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.h deleted file mode 100644 index dbd676b5e95..00000000000 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.h +++ /dev/null @@ -1,64 +0,0 @@ -#ifndef ThorVGCanvasImpl_h -#define ThorVGCanvasImpl_h - -#include "Canvas.h" - -#include - -namespace lottieRendering { - -class ThorVGCanvasImpl: public Canvas { -public: - ThorVGCanvasImpl(int width, int height); - virtual ~ThorVGCanvasImpl(); - - virtual int width() const override; - virtual int height() const override; - - virtual std::shared_ptr makeLayer(int width, int height) override; - - virtual void saveState() override; - virtual void restoreState() override; - - virtual void fillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Color const &color) override; - virtual void linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottieRendering::Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override; - virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottieRendering::Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override; - virtual void strokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, lottie::Color const &color) override; - virtual void linearGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override; - virtual void radialGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override; - virtual void fill(lottie::CGRect const &rect, lottie::Color const &fillColor) override; - - virtual void setBlendMode(BlendMode blendMode) override; - - virtual void setAlpha(float alpha) override; - - virtual void concatenate(lottie::Transform2D const &transform) override; - - virtual void draw(std::shared_ptr const &other, lottie::CGRect const &rect) override; - - uint32_t *backingData() { - return _backingData; - } - - int bytesPerRow() const { - return _bytesPerRow; - } - - void flush(); - -private: - int _width = 0; - int _height = 0; - std::unique_ptr _canvas; - - float _alpha = 1.0; - lottie::Transform2D _transform; - std::vector _stateStack; - int _bytesPerRow = 0; - uint32_t *_backingData = nullptr; - int _statsNumStrokes = 0; -}; - -} - -#endif diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.mm b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.mm deleted file mode 100644 index 4be6064df21..00000000000 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.mm +++ /dev/null @@ -1,288 +0,0 @@ -#include "ThorVGCanvasImpl.h" - -namespace lottieRendering { - -namespace { - -void tvgPath(CanvasPathEnumerator const &enumeratePath, tvg::Shape *shape) { - enumeratePath([&](PathCommand const &command) { - switch (command.type) { - case PathCommandType::MoveTo: { - shape->moveTo(command.points[0].x, command.points[0].y); - break; - } - case PathCommandType::LineTo: { - shape->lineTo(command.points[0].x, command.points[0].y); - break; - } - case PathCommandType::CurveTo: { - shape->cubicTo(command.points[0].x, command.points[0].y, command.points[1].x, command.points[1].y, command.points[2].x, command.points[2].y); - break; - } - case PathCommandType::Close: { - shape->close(); - break; - } - } - }); -} - -tvg::Matrix tvgTransform(lottie::Transform2D const &transform) { - CGAffineTransform affineTransform = CATransform3DGetAffineTransform(lottie::nativeTransform(transform)); - tvg::Matrix result; - result.e11 = affineTransform.a; - result.e21 = affineTransform.b; - result.e31 = 0.0f; - result.e12 = affineTransform.c; - result.e22 = affineTransform.d; - result.e32 = 0.0f; - result.e13 = affineTransform.tx; - result.e23 = affineTransform.ty; - result.e33 = 1.0f; - return result; -} - -} - -ThorVGCanvasImpl::ThorVGCanvasImpl(int width, int height) : -_width(width), _height(height), _transform(lottie::Transform2D::identity()) { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - tvg::Initializer::init(0); - }); - - _canvas = tvg::SwCanvas::gen(); - - _bytesPerRow = width * 4; - - static uint32_t *sharedBackingData = (uint32_t *)malloc(_bytesPerRow * height); - _backingData = sharedBackingData; - - _canvas->target(_backingData, _bytesPerRow / 4, width, height, tvg::SwCanvas::ARGB8888); -} - -ThorVGCanvasImpl::~ThorVGCanvasImpl() { -} - -int ThorVGCanvasImpl::width() const { - return _width; -} - -int ThorVGCanvasImpl::height() const { - return _height; -} - -std::shared_ptr ThorVGCanvasImpl::makeLayer(int width, int height) { - return std::make_shared(width, height); -} - -void ThorVGCanvasImpl::saveState() { - _stateStack.push_back(_transform); -} - -void ThorVGCanvasImpl::restoreState() { - if (_stateStack.empty()) { - assert(false); - return; - } - _transform = _stateStack[_stateStack.size() - 1]; - _stateStack.pop_back(); -} - -void ThorVGCanvasImpl::fillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Color const &color) { - auto shape = tvg::Shape::gen(); - tvgPath(enumeratePath, shape.get()); - - shape->transform(tvgTransform(_transform)); - - shape->fill((int)(color.r * 255.0), (int)(color.g * 255.0), (int)(color.b * 255.0), (int)(color.a * _alpha * 255.0)); - shape->fill(fillRule == lottie::FillRule::EvenOdd ? tvg::FillRule::EvenOdd : tvg::FillRule::Winding); - - _canvas->push(std::move(shape)); -} - -void ThorVGCanvasImpl::linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) { - auto shape = tvg::Shape::gen(); - tvgPath(enumeratePath, shape.get()); - - shape->transform(tvgTransform(_transform)); - - auto fill = tvg::LinearGradient::gen(); - fill->linear(start.x, start.y, end.x, end.y); - - std::vector colors; - for (size_t i = 0; i < gradient.colors().size(); i++) { - const auto &color = gradient.colors()[i]; - tvg::Fill::ColorStop colorStop; - colorStop.offset = gradient.locations()[i]; - colorStop.r = (int)(color.r * 255.0); - colorStop.g = (int)(color.g * 255.0); - colorStop.b = (int)(color.b * 255.0); - colorStop.a = (int)(color.a * _alpha * 255.0); - colors.push_back(colorStop); - } - fill->colorStops(colors.data(), (uint32_t)colors.size()); - shape->fill(std::move(fill)); - - shape->fill(fillRule == lottie::FillRule::EvenOdd ? tvg::FillRule::EvenOdd : tvg::FillRule::Winding); - - _canvas->push(std::move(shape)); -} - -void ThorVGCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) { - auto shape = tvg::Shape::gen(); - tvgPath(enumeratePath, shape.get()); - - shape->transform(tvgTransform(_transform)); - - auto fill = tvg::RadialGradient::gen(); - fill->radial(startCenter.x, startCenter.y, endRadius); - - std::vector colors; - for (size_t i = 0; i < gradient.colors().size(); i++) { - const auto &color = gradient.colors()[i]; - tvg::Fill::ColorStop colorStop; - colorStop.offset = gradient.locations()[i]; - colorStop.r = (int)(color.r * 255.0); - colorStop.g = (int)(color.g * 255.0); - colorStop.b = (int)(color.b * 255.0); - colorStop.a = (int)(color.a * _alpha * 255.0); - colors.push_back(colorStop); - } - fill->colorStops(colors.data(), (uint32_t)colors.size()); - shape->fill(std::move(fill)); - - shape->fill(fillRule == lottie::FillRule::EvenOdd ? tvg::FillRule::EvenOdd : tvg::FillRule::Winding); - - _canvas->push(std::move(shape)); -} - -void ThorVGCanvasImpl::strokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, lottie::Color const &color) { - auto shape = tvg::Shape::gen(); - tvgPath(enumeratePath, shape.get()); - - shape->transform(tvgTransform(_transform)); - - shape->strokeFill((int)(color.r * 255.0), (int)(color.g * 255.0), (int)(color.b * 255.0), (int)(color.a * _alpha * 255.0)); - shape->strokeWidth(lineWidth); - - switch (lineJoin) { - case lottie::LineJoin::Miter: { - shape->strokeJoin(tvg::StrokeJoin::Miter); - break; - } - case lottie::LineJoin::Round: { - shape->strokeJoin(tvg::StrokeJoin::Round); - break; - } - case lottie::LineJoin::Bevel: { - shape->strokeJoin(tvg::StrokeJoin::Bevel); - break; - } - default: { - shape->strokeJoin(tvg::StrokeJoin::Bevel); - break; - } - } - - switch (lineCap) { - case lottie::LineCap::Butt: { - shape->strokeCap(tvg::StrokeCap::Butt); - break; - } - case lottie::LineCap::Round: { - shape->strokeCap(tvg::StrokeCap::Round); - break; - } - case lottie::LineCap::Square: { - shape->strokeCap(tvg::StrokeCap::Square); - break; - } - default: { - shape->strokeCap(tvg::StrokeCap::Square); - break; - } - } - - if (!dashPattern.empty()) { - std::vector intervals; - intervals.reserve(dashPattern.size()); - for (auto value : dashPattern) { - intervals.push_back(value); - } - shape->strokeDash(intervals.data(), (uint32_t)intervals.size()); - //TODO:phase - } - - _canvas->push(std::move(shape)); -} - -void ThorVGCanvasImpl::linearGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) { -} - -void ThorVGCanvasImpl::radialGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) { -} - -void ThorVGCanvasImpl::fill(lottie::CGRect const &rect, lottie::Color const &fillColor) { - auto shape = tvg::Shape::gen(); - shape->appendRect(rect.x, rect.y, rect.width, rect.height, 0.0f, 0.0f); - - shape->transform(tvgTransform(_transform)); - - shape->fill((int)(fillColor.r * 255.0), (int)(fillColor.g * 255.0), (int)(fillColor.b * 255.0), (int)(fillColor.a * _alpha * 255.0)); - - _canvas->push(std::move(shape)); -} - -void ThorVGCanvasImpl::setBlendMode(BlendMode blendMode) { - /*switch (blendMode) { - case CGBlendMode::Normal: { - _blendMode = SkBlendMode::kSrcOver; - break; - } - case CGBlendMode::DestinationIn: { - _blendMode = SkBlendMode::kDstIn; - break; - } - case CGBlendMode::DestinationOut: { - _blendMode = SkBlendMode::kDstOut; - break; - } - default: { - _blendMode = SkBlendMode::kSrcOver; - break; - } - }*/ -} - -void ThorVGCanvasImpl::setAlpha(float alpha) { - _alpha = alpha; -} - -void ThorVGCanvasImpl::concatenate(lottie::Transform2D const &transform) { - _transform = transform * _transform; - /*_canvas->concat(SkM44( - transform.m11, transform.m21, transform.m31, transform.m41, - transform.m12, transform.m22, transform.m32, transform.m42, - transform.m13, transform.m23, transform.m33, transform.m43, - transform.m14, transform.m24, transform.m34, transform.m44 - ));*/ -} - -void ThorVGCanvasImpl::draw(std::shared_ptr const &other, lottie::CGRect const &rect) { - /*ThorVGCanvasImpl *impl = (ThorVGCanvasImpl *)other.get(); - auto image = impl->surface()->makeImageSnapshot(); - SkPaint paint; - paint.setBlendMode(_blendMode); - paint.setAlphaf(_alpha); - _canvas->drawImageRect(image.get(), SkRect::MakeXYWH(rect.x, rect.y, rect.width, rect.height), SkSamplingOptions(SkFilterMode::kLinear), &paint);*/ -} - -void ThorVGCanvasImpl::flush() { - _canvas->draw(); - _canvas->sync(); - - _statsNumStrokes = 0; -} - -} diff --git a/Tests/LottieMetalTest/Sources/CompareToReferenceRendering.swift b/Tests/LottieMetalTest/Sources/CompareToReferenceRendering.swift index 7b67ef5470c..78d3ef49c47 100644 --- a/Tests/LottieMetalTest/Sources/CompareToReferenceRendering.swift +++ b/Tests/LottieMetalTest/Sources/CompareToReferenceRendering.swift @@ -10,17 +10,26 @@ import SoftwareLottieRenderer import LottieSwift @available(iOS 13.0, *) -func areImagesEqual(_ lhs: UIImage, _ rhs: UIImage) -> UIImage? { +func areImagesEqual(_ lhs: UIImage, _ rhs: UIImage, allowedDifference: Double) -> (UIImage?, UIImage) { let lhsBuffer = try! vImage_Buffer(cgImage: lhs.cgImage!) let rhsBuffer = try! vImage_Buffer(cgImage: rhs.cgImage!) + let deltaBuffer = try! vImage_Buffer(cgImage: lhs.cgImage!) + defer { + lhsBuffer.free() + rhsBuffer.free() + deltaBuffer.free() + } + + memset(deltaBuffer.data, 0, Int(deltaBuffer.height) * deltaBuffer.rowBytes) - let maxDifferenceCount = Int((Double(Int(lhs.size.width) * Int(lhs.size.height)) * 0.01)) + let maxDifferenceCount = Int((Double(Int(lhs.size.width) * Int(lhs.size.height)) * allowedDifference)) var foundDifferenceCount = 0 outer: for y in 0 ..< Int(lhs.size.height) { let lhsRowPixels = lhsBuffer.data.assumingMemoryBound(to: UInt8.self).advanced(by: y * lhsBuffer.rowBytes) let rhsRowPixels = rhsBuffer.data.assumingMemoryBound(to: UInt8.self).advanced(by: y * lhsBuffer.rowBytes) + let deltaRowPixels = deltaBuffer.data.assumingMemoryBound(to: UInt8.self).advanced(by: y * lhsBuffer.rowBytes) for x in 0 ..< Int(lhs.size.width) { let lhs0 = lhsRowPixels.advanced(by: x * 4 + 0).pointee @@ -36,69 +45,65 @@ func areImagesEqual(_ lhs: UIImage, _ rhs: UIImage) -> UIImage? { let maxDiff = 25 if abs(Int(lhs0) - Int(rhs0)) > maxDiff || abs(Int(lhs1) - Int(rhs1)) > maxDiff || abs(Int(lhs2) - Int(rhs2)) > maxDiff || abs(Int(lhs3) - Int(rhs3)) > maxDiff { - /*if false { - lhsRowPixels.advanced(by: x * 4 + 0).pointee = 255 - lhsRowPixels.advanced(by: x * 4 + 1).pointee = 0 - lhsRowPixels.advanced(by: x * 4 + 2).pointee = 0 - lhsRowPixels.advanced(by: x * 4 + 3).pointee = 255 - }*/ + deltaRowPixels.advanced(by: x * 4 + 0).pointee = 255 + deltaRowPixels.advanced(by: x * 4 + 1).pointee = 0 + deltaRowPixels.advanced(by: x * 4 + 2).pointee = 0 + deltaRowPixels.advanced(by: x * 4 + 3).pointee = 255 foundDifferenceCount += 1 } } } - lhsBuffer.free() - rhsBuffer.free() + let colorSpace = Unmanaged.passRetained(lhs.cgImage!.colorSpace!) + let deltaImage = try! deltaBuffer.createCGImage(format: vImage_CGImageFormat(bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: colorSpace, bitmapInfo: lhs.cgImage!.bitmapInfo, version: 0, decode: nil, renderingIntent: .defaultIntent), flags: .doNotTile) if foundDifferenceCount > maxDifferenceCount { - let colorSpace = Unmanaged.passRetained(lhs.cgImage!.colorSpace!) let diffImage = try! lhsBuffer.createCGImage(format: vImage_CGImageFormat(bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: colorSpace, bitmapInfo: lhs.cgImage!.bitmapInfo, version: 0, decode: nil, renderingIntent: .defaultIntent), flags: .doNotTile) - return UIImage(cgImage: diffImage) + + return (UIImage(cgImage: diffImage), UIImage(cgImage: deltaImage)) } else { - return nil + return (nil, UIImage(cgImage: deltaImage)) } } @available(iOS 13.0, *) -func processDrawAnimation(baseCachePath: String, path: String, name: String, size: CGSize, alwaysDraw: Bool, updateImage: @escaping (UIImage?, UIImage?) -> Void) async -> Bool { +func processDrawAnimation(baseCachePath: String, path: String, name: String, size: CGSize, allowedDifference: Double, alwaysDraw: Bool, useNonReferenceRendering: Bool, updateImage: @escaping (UIImage?, UIImage?, UIImage?) -> Void) async -> Bool { guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { print("Could not load \(path)") return false } - guard let animation = LottieAnimation(data: data) else { - print("Could not parse animation at \(path)") - return false - } - - let layer = LottieAnimationContainer(animation: animation) - let cacheFolderPath = cacheReferenceFolderPath(baseCachePath: baseCachePath, width: Int(size.width), name: name) if !FileManager.default.fileExists(atPath: cacheFolderPath) { let _ = await cacheReferenceAnimation(baseCachePath: baseCachePath, width: Int(size.width), path: path, name: name) } - let renderer = SoftwareLottieRenderer(animationContainer: layer) + guard let renderer = SoftwareLottieRenderer(data: data) else { + print("Could not parse animation at \(path)") + return false + } - for i in 0 ..< min(100000, animation.frameCount) { + for i in 0 ..< min(100000, renderer.frameCount) { let frameResult = autoreleasepool { - let frameIndex = i % animation.frameCount + let frameIndex = i % renderer.frameCount let referenceImageData = try! Data(contentsOf: URL(fileURLWithPath: cacheFolderPath + "/frame\(frameIndex)")) let referenceImage = decompressImageFrame(data: referenceImageData) - layer.update(frameIndex) - let image = renderer.render(for: size, useReferenceRendering: true)! + renderer.setFrame(CGFloat(frameIndex)) + let image = renderer.render(for: size, useReferenceRendering: !useNonReferenceRendering, canUseMoreMemory: false, skipImageGeneration: false)! - if let diffImage = areImagesEqual(image, referenceImage) { - updateImage(diffImage, referenceImage) + let (diffImage, deltaImage) = areImagesEqual(image, referenceImage, allowedDifference: allowedDifference) + + if !useNonReferenceRendering, let diffImage { + updateImage(diffImage, referenceImage, deltaImage) print("Mismatch in frame \(frameIndex)") return false } else { if alwaysDraw { - updateImage(image, referenceImage) + updateImage(image, referenceImage, diffImage) } return true } @@ -283,6 +288,43 @@ func decompressImageFrame(data: Data) -> UIImage { return decodeImageQOI(data)! } +final class ReferenceLottieAnimationItem { + private let referenceAnimation: Animation + private let referenceLayer: MainThreadAnimationLayer + let frameCount: Int + + init?(path: String) { + guard let referenceAnimation = Animation.filepath(path) else { + return nil + } + self.referenceAnimation = referenceAnimation + + self.referenceLayer = MainThreadAnimationLayer(animation: referenceAnimation, imageProvider: BlankImageProvider(), textProvider: DefaultTextProvider(), fontProvider: DefaultFontProvider()) + self.referenceLayer.position = referenceAnimation.bounds.center + self.referenceLayer.isOpaque = false + self.referenceLayer.backgroundColor = nil + + self.frameCount = Int(referenceAnimation.endFrame - referenceAnimation.startFrame) + } + + func setFrame(index: Int) { + self.referenceLayer.currentFrame = self.referenceAnimation.startFrame + CGFloat(index) + self.referenceLayer.displayUpdate() + } + + func makeImage(width: Int, height: Int) -> UIImage? { + let size = CGSize(width: CGFloat(width), height: CGFloat(width)) + + let referenceContext = ImageContext(width: width, height: height) + referenceContext.context.clear(CGRect(origin: CGPoint(), size: size)) + referenceContext.context.scaleBy(x: size.width / CGFloat(self.referenceAnimation.width), y: size.height / CGFloat(self.referenceAnimation.height)) + + referenceLayer.render(in: referenceContext.context) + + return referenceContext.makeImage() + } +} + @MainActor func cacheReferenceAnimation(baseCachePath: String, width: Int, path: String, name: String) -> String { let targetFolderPath = cacheReferenceFolderPath(baseCachePath: baseCachePath, width: width, name: name) @@ -290,34 +332,19 @@ func cacheReferenceAnimation(baseCachePath: String, width: Int, path: String, na return targetFolderPath } - guard let referenceAnimation = Animation.filepath(path) else { - preconditionFailure("Could not parse reference animation at \(path)") + guard let referenceItem = ReferenceLottieAnimationItem(path: path) else { + preconditionFailure("Could not load reference animation at \(path)") } - let referenceLayer = MainThreadAnimationLayer(animation: referenceAnimation, imageProvider: BlankImageProvider(), textProvider: DefaultTextProvider(), fontProvider: DefaultFontProvider()) let cacheFolderPath = NSTemporaryDirectory() + "\(UInt64.random(in: 0 ... UInt64.max))" let _ = try? FileManager.default.createDirectory(atPath: cacheFolderPath, withIntermediateDirectories: true) - let frameCount = Int(referenceAnimation.endFrame - referenceAnimation.startFrame) - - let size = CGSize(width: CGFloat(width), height: CGFloat(width)) - - for i in 0 ..< min(100000, frameCount) { - let frameIndex = i % frameCount + for i in 0 ..< min(100000, referenceItem.frameCount) { + let frameIndex = i % referenceItem.frameCount - referenceLayer.currentFrame = CGFloat(frameIndex) - referenceLayer.displayUpdate() - referenceLayer.position = referenceAnimation.bounds.center - - referenceLayer.isOpaque = false - referenceLayer.backgroundColor = nil - let referenceContext = ImageContext(width: width, height: width) - referenceContext.context.clear(CGRect(origin: CGPoint(), size: size)) - referenceContext.context.scaleBy(x: size.width / CGFloat(referenceAnimation.width), y: size.height / CGFloat(referenceAnimation.height)) - - referenceLayer.render(in: referenceContext.context) + referenceItem.setFrame(index: frameIndex) - let referenceImage = referenceContext.makeImage() + let referenceImage = referenceItem.makeImage(width: width, height: width)! try! compressImageFrame(image: referenceImage).write(to: URL(fileURLWithPath: cacheFolderPath + "/frame\(i)")) } diff --git a/Tests/LottieMetalTest/Sources/ViewController.swift b/Tests/LottieMetalTest/Sources/ViewController.swift index 3fa91463c3f..af9a1224462 100644 --- a/Tests/LottieMetalTest/Sources/ViewController.swift +++ b/Tests/LottieMetalTest/Sources/ViewController.swift @@ -13,8 +13,9 @@ private final class ReferenceCompareTest { private let view: UIView private let imageView = UIImageView() private let referenceImageView = UIImageView() + private let deltaImageView = UIImageView() - init(view: UIView) { + init(view: UIView, testNonReference: Bool) { lottieSwift_getPathNativeBoundingBox = { path in return getPathNativeBoundingBox(path) } @@ -37,6 +38,12 @@ private final class ReferenceCompareTest { self.referenceImageView.backgroundColor = self.view.backgroundColor self.referenceImageView.transform = CGAffineTransform.init(scaleX: 1.0, y: -1.0) + self.view.addSubview(self.deltaImageView) + self.deltaImageView.layer.magnificationFilter = .nearest + self.deltaImageView.frame = CGRect(origin: CGPoint(x: 10.0, y: topInset + 256.0 + 1.0 + 256.0 + 1.0), size: CGSize(width: 256.0, height: 256.0)) + self.deltaImageView.backgroundColor = self.view.backgroundColor + self.deltaImageView.transform = CGAffineTransform.init(scaleX: 1.0, y: -1.0) + let bundlePath = Bundle.main.path(forResource: "TestDataBundle", ofType: "bundle")! Task.detached { @@ -67,6 +74,9 @@ private final class ReferenceCompareTest { "1391391008142393350.json": 1024 ] + let allowedDifferences: [String: Double] = [ + "1258816259754165.json": 0.04 + ] let defaultSize = 128 let baseCachePath = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).path + "/frame-cache" @@ -78,9 +88,9 @@ private final class ReferenceCompareTest { } var continueFromName: String? - //continueFromName = "35707580709863498.json" + //continueFromName = "562563904580878375.json" - let _ = await processAnimationFolderAsync(basePath: bundlePath, path: "", stopOnFailure: true, process: { path, name, alwaysDraw in + let _ = await processAnimationFolderAsync(basePath: bundlePath, path: "", stopOnFailure: !testNonReference, process: { path, name, alwaysDraw in if let continueFromNameValue = continueFromName { if continueFromNameValue == name { continueFromName = nil @@ -91,10 +101,11 @@ private final class ReferenceCompareTest { let size = sizeMapping[name] ?? defaultSize - let result = await processDrawAnimation(baseCachePath: baseCachePath, path: path, name: name, size: CGSize(width: size, height: size), alwaysDraw: alwaysDraw, updateImage: { image, referenceImage in + let result = await processDrawAnimation(baseCachePath: baseCachePath, path: path, name: name, size: CGSize(width: size, height: size), allowedDifference: allowedDifferences[name] ?? 0.01, alwaysDraw: alwaysDraw, useNonReferenceRendering: testNonReference, updateImage: { image, referenceImage, differenceImage in DispatchQueue.main.async { self.imageView.image = image self.referenceImageView.image = referenceImage + self.deltaImageView.image = differenceImage } }) return result @@ -103,6 +114,143 @@ private final class ReferenceCompareTest { } } +@available(iOS 13.0, *) +private final class ManualReferenceCompareTest { + private final class Item { + let renderer: SoftwareLottieRenderer + let referenceRenderer: ReferenceLottieAnimationItem + + init(renderer: SoftwareLottieRenderer, referenceRenderer: ReferenceLottieAnimationItem) { + self.renderer = renderer + self.referenceRenderer = referenceRenderer + } + } + + private let view: UIView + private let imageView = UIImageView() + private let referenceImageView = UIImageView() + private let labelView = UILabel() + + private let renderSize: CGSize + private let testNonReference: Bool + + private let fileList: [(filePath: String, fileName: String)] + private var currentFileIndex: Int = 0 + private var currentItem: Item? + + private var frameDisplayLink: SharedDisplayLinkDriver.Link? + + init(view: UIView) { + self.testNonReference = true + + self.currentFileIndex = 0 + + lottieSwift_getPathNativeBoundingBox = { path in + return getPathNativeBoundingBox(path) + } + + let bundlePath = Bundle.main.path(forResource: "TestDataBundle", ofType: "bundle")! + self.fileList = buildAnimationFolderItems(basePath: bundlePath, path: "") + + if let index = self.fileList.firstIndex(where: { $0.fileName == "shit.json" }) { + self.currentFileIndex = index + } + + self.renderSize = CGSize(width: 256.0, height: 256.0) + + self.view = view + self.view.backgroundColor = .white + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + + let topInset: CGFloat = 50.0 + + self.view.addSubview(self.imageView) + self.imageView.layer.magnificationFilter = .nearest + self.imageView.frame = CGRect(origin: CGPoint(x: 10.0, y: topInset), size: CGSize(width: 256.0, height: 256.0)) + self.imageView.backgroundColor = self.view.backgroundColor + self.imageView.transform = CGAffineTransform.init(scaleX: 1.0, y: -1.0) + + self.view.addSubview(self.referenceImageView) + self.referenceImageView.layer.magnificationFilter = .nearest + self.referenceImageView.frame = CGRect(origin: CGPoint(x: 10.0, y: topInset + 256.0 + 1.0), size: CGSize(width: 256.0, height: 256.0)) + self.referenceImageView.backgroundColor = self.view.backgroundColor + self.referenceImageView.transform = CGAffineTransform.init(scaleX: 1.0, y: -1.0) + + self.view.addSubview(self.labelView) + + self.updateCurrentAnimation() + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if recognizer.location(in: self.view).x <= self.view.bounds.width * 0.5 { + if self.currentFileIndex != 0 { + self.currentFileIndex = self.currentFileIndex - 1 + } + } else { + self.currentFileIndex = (self.currentFileIndex + 1) % self.fileList.count + } + self.updateCurrentAnimation() + } + } + + private func updateCurrentAnimation() { + self.imageView.image = nil + self.referenceImageView.image = nil + self.currentItem = nil + + self.labelView.text = "\(self.currentFileIndex + 1) / \(self.fileList.count)" + self.labelView.sizeToFit() + self.labelView.center = CGPoint(x: self.view.bounds.midX, y: self.view.bounds.height - 10.0 - self.labelView.bounds.height) + + self.frameDisplayLink?.invalidate() + self.frameDisplayLink = nil + + let (filePath, _) = self.fileList[self.currentFileIndex] + + guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { + print("Could not load \(filePath)") + return + } + guard let renderer = SoftwareLottieRenderer(data: data) else { + print("Could not load animation at \(filePath)") + return + } + guard let referenceRenderer = ReferenceLottieAnimationItem(path: filePath) else { + print("Could not load reference animation at \(filePath)") + return + } + + let currentItem = Item(renderer: renderer, referenceRenderer: referenceRenderer) + self.currentItem = currentItem + + var animationTime = 0.0 + let secondsPerFrame = 1.0 / Double(renderer.framesPerSecond) + + let frameDisplayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in + guard let self, let currentItem = self.currentItem else { + return + } + + var frameIndex = animationTime / secondsPerFrame + frameIndex = frameIndex.truncatingRemainder(dividingBy: Double(currentItem.renderer.frameCount)) + + currentItem.renderer.setFrame(frameIndex) + let image = currentItem.renderer.render(for: self.renderSize, useReferenceRendering: !self.testNonReference, canUseMoreMemory: false, skipImageGeneration: false)! + self.imageView.image = image + + currentItem.referenceRenderer.setFrame(index: Int(frameIndex)) + let referenceImage = currentItem.referenceRenderer.makeImage(width: Int(self.renderSize.width), height: Int(self.renderSize.height))! + self.referenceImageView.image = referenceImage + + animationTime += deltaTime + }) + self.frameDisplayLink = frameDisplayLink + frameDisplayLink.isPaused = false + } +} + public final class ViewController: UIViewController { private var link: SharedDisplayLinkDriver.Link? private var test: AnyObject? @@ -113,18 +261,22 @@ public final class ViewController: UIViewController { SharedDisplayLinkDriver.shared.updateForegroundState(true) let bundlePath = Bundle.main.path(forResource: "TestDataBundle", ofType: "bundle")! - let filePath = bundlePath + "/fireworks.json" + let filePath = bundlePath + "/fire.json" - let performanceFrameSize = 8 + let performanceFrameSize = 128 self.view.layer.addSublayer(MetalEngine.shared.rootLayer) - if "".isEmpty { + if !"".isEmpty { if #available(iOS 13.0, *) { - self.test = ReferenceCompareTest(view: self.view) + self.test = ReferenceCompareTest(view: self.view, testNonReference: false) + } + } else if "".isEmpty { + if #available(iOS 13.0, *) { + self.test = ManualReferenceCompareTest(view: self.view) } } else if !"".isEmpty { - let cachedAnimation = cacheLottieMetalAnimation(path: filePath)! + /*let cachedAnimation = cacheLottieMetalAnimation(path: filePath)! let animation = parseCachedLottieMetalAnimation(data: cachedAnimation)! /*let animationData = try! Data(contentsOf: URL(fileURLWithPath: filePath)) @@ -146,29 +298,23 @@ public final class ViewController: UIViewController { self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { _ in lottieLayer.frameIndex = (lottieLayer.frameIndex + 1) % animation.frameCount lottieLayer.setNeedsUpdate() - }) + })*/ } else if "".isEmpty { Thread { let animationData = try! Data(contentsOf: URL(fileURLWithPath: filePath)) var startTime = CFAbsoluteTimeGetCurrent() - let animation = LottieAnimation(data: animationData)! - print("Load time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") - - startTime = CFAbsoluteTimeGetCurrent() - let animationContainer = LottieAnimationContainer(animation: animation) - animationContainer.update(0) - print("Build time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") - let animationRenderer = SoftwareLottieRenderer(animationContainer: animationContainer) + let animationRenderer = SoftwareLottieRenderer(data: animationData)! + print("Load time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") startTime = CFAbsoluteTimeGetCurrent() var numUpdates: Int = 0 var frameIndex = 0 while true { - animationContainer.update(frameIndex) - let _ = animationRenderer.render(for: CGSize(width: CGFloat(performanceFrameSize), height: CGFloat(performanceFrameSize)), useReferenceRendering: false) - frameIndex = (frameIndex + 1) % animationContainer.animation.frameCount + animationRenderer.setFrame(CGFloat(frameIndex)) + let _ = animationRenderer.render(for: CGSize(width: CGFloat(performanceFrameSize), height: CGFloat(performanceFrameSize)), useReferenceRendering: false, canUseMoreMemory: true, skipImageGeneration: true) + frameIndex = (frameIndex + 1) % animationRenderer.frameCount numUpdates += 1 let timestamp = CFAbsoluteTimeGetCurrent() let deltaTime = timestamp - startTime diff --git a/Tests/LottieMetalTest/skia/BUILD b/Tests/LottieMetalTest/skia/BUILD new file mode 100644 index 00000000000..540ec8688e4 --- /dev/null +++ b/Tests/LottieMetalTest/skia/BUILD @@ -0,0 +1,49 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +load("@build_bazel_rules_apple//apple:apple.bzl", + "apple_dynamic_framework_import", +) + +framework_imports = select({ + "@build_bazel_rules_apple//apple:ios_arm64": glob([ + "device/libskia.framework/**" + ]), + "//build-system:ios_sim_arm64": glob([ + "simulator/libskia.framework/**" + ]) +}) + +apple_dynamic_framework_import( + name = "libskia", + framework_imports = framework_imports, + visibility = ["//visibility:public"], +) + +objc_library( + name = "skia", + enable_modules = True, + module_name = "skia", + srcs = glob([ + ]), + copts = [ + "-I{}/PublicHeaders/skia".format(package_name()), + ], + linkopts = [ + "-Wl,-rpath,@loader_path/Frameworks/libskia.framework", + ], + hdrs = glob([ + "PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + "PublicHeaders/skia", + ], + deps = [ + ":libskia", + ], + sdk_frameworks = [ + "Foundation", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/OWNERS b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/OWNERS new file mode 100644 index 00000000000..0d7fbad28ac --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/OWNERS @@ -0,0 +1,15 @@ +set noparent + +# Include one of the following reviewers for CLs that add or change Skia's public API: +brianosman@google.com +egdaniel@google.com +fmalita@google.com +fmalita@chromium.org +hcm@google.com +herb@google.com +robertphillips@google.com + +per-file BUILD.bazel=bungeman@google.com +per-file BUILD.bazel=jcgregorio@google.com +per-file BUILD.bazel=kjlubick@google.com +per-file BUILD.bazel=lovisolo@google.com diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkAndroidCodec.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkAndroidCodec.h new file mode 100644 index 00000000000..2b8a79751cf --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkAndroidCodec.h @@ -0,0 +1,297 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkAndroidCodec_DEFINED +#define SkAndroidCodec_DEFINED + +#include "include/codec/SkCodec.h" +#include "include/core/SkColorSpace.h" +#include "include/core/SkImageInfo.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkSize.h" +#include "include/core/SkTypes.h" +#include "include/private/SkEncodedInfo.h" +#include "include/private/base/SkNoncopyable.h" +#include "modules/skcms/skcms.h" + +// TODO(kjlubick, bungeman) Replace these includes with forward declares +#include "include/codec/SkEncodedImageFormat.h" // IWYU pragma: keep +#include "include/core/SkAlphaType.h" // IWYU pragma: keep +#include "include/core/SkColorType.h" // IWYU pragma: keep + +#include +#include + +class SkData; +class SkPngChunkReader; +class SkStream; +struct SkGainmapInfo; +struct SkIRect; + +/** + * Abstract interface defining image codec functionality that is necessary for + * Android. + */ +class SK_API SkAndroidCodec : SkNoncopyable { +public: + /** + * Deprecated. + * + * Now that SkAndroidCodec supports multiframe images, there are multiple + * ways to handle compositing an oriented frame on top of an oriented frame + * with different tradeoffs. SkAndroidCodec now ignores the orientation and + * forces the client to handle it. + */ + enum class ExifOrientationBehavior { + kIgnore, + kRespect, + }; + + /** + * Pass ownership of an SkCodec to a newly-created SkAndroidCodec. + */ + static std::unique_ptr MakeFromCodec(std::unique_ptr); + + /** + * If this stream represents an encoded image that we know how to decode, + * return an SkAndroidCodec that can decode it. Otherwise return NULL. + * + * The SkPngChunkReader handles unknown chunks in PNGs. + * See SkCodec.h for more details. + * + * If NULL is returned, the stream is deleted immediately. Otherwise, the + * SkCodec takes ownership of it, and will delete it when done with it. + */ + static std::unique_ptr MakeFromStream(std::unique_ptr, + SkPngChunkReader* = nullptr); + + /** + * If this data represents an encoded image that we know how to decode, + * return an SkAndroidCodec that can decode it. Otherwise return NULL. + * + * The SkPngChunkReader handles unknown chunks in PNGs. + * See SkCodec.h for more details. + */ + static std::unique_ptr MakeFromData(sk_sp, SkPngChunkReader* = nullptr); + + virtual ~SkAndroidCodec(); + + // TODO: fInfo is now just a cache of SkCodec's SkImageInfo. No need to + // cache and return a reference here, once Android call-sites are updated. + const SkImageInfo& getInfo() const { return fInfo; } + + /** + * Return the ICC profile of the encoded data. + */ + const skcms_ICCProfile* getICCProfile() const { + return fCodec->getEncodedInfo().profile(); + } + + /** + * Format of the encoded data. + */ + SkEncodedImageFormat getEncodedFormat() const { return fCodec->getEncodedFormat(); } + + /** + * @param requestedColorType Color type requested by the client + * + * |requestedColorType| may be overriden. We will default to kF16 + * for high precision images. + * + * In the general case, if it is possible to decode to + * |requestedColorType|, this returns |requestedColorType|. + * Otherwise, this returns a color type that is an appropriate + * match for the the encoded data. + */ + SkColorType computeOutputColorType(SkColorType requestedColorType); + + /** + * @param requestedUnpremul Indicates if the client requested + * unpremultiplied output + * + * Returns the appropriate alpha type to decode to. If the image + * has alpha, the value of requestedUnpremul will be honored. + */ + SkAlphaType computeOutputAlphaType(bool requestedUnpremul); + + /** + * @param outputColorType Color type that the client will decode to. + * @param prefColorSpace Preferred color space to decode to. + * This may not return |prefColorSpace| for + * specific color types. + * + * Returns the appropriate color space to decode to. + */ + sk_sp computeOutputColorSpace(SkColorType outputColorType, + sk_sp prefColorSpace = nullptr); + + /** + * Compute the appropriate sample size to get to |size|. + * + * @param size As an input parameter, the desired output size of + * the decode. As an output parameter, the smallest sampled size + * larger than the input. + * @return the sample size to set AndroidOptions::fSampleSize to decode + * to the output |size|. + */ + int computeSampleSize(SkISize* size) const; + + /** + * Returns the dimensions of the scaled output image, for an input + * sampleSize. + * + * When the sample size divides evenly into the original dimensions, the + * scaled output dimensions will simply be equal to the original + * dimensions divided by the sample size. + * + * When the sample size does not divide even into the original + * dimensions, the codec may round up or down, depending on what is most + * efficient to decode. + * + * Finally, the codec will always recommend a non-zero output, so the output + * dimension will always be one if the sampleSize is greater than the + * original dimension. + */ + SkISize getSampledDimensions(int sampleSize) const; + + /** + * Return (via desiredSubset) a subset which can decoded from this codec, + * or false if the input subset is invalid. + * + * @param desiredSubset in/out parameter + * As input, a desired subset of the original bounds + * (as specified by getInfo). + * As output, if true is returned, desiredSubset may + * have been modified to a subset which is + * supported. Although a particular change may have + * been made to desiredSubset to create something + * supported, it is possible other changes could + * result in a valid subset. If false is returned, + * desiredSubset's value is undefined. + * @return true If the input desiredSubset is valid. + * desiredSubset may be modified to a subset + * supported by the codec. + * false If desiredSubset is invalid (NULL or not fully + * contained within the image). + */ + bool getSupportedSubset(SkIRect* desiredSubset) const; + // TODO: Rename SkCodec::getValidSubset() to getSupportedSubset() + + /** + * Returns the dimensions of the scaled, partial output image, for an + * input sampleSize and subset. + * + * @param sampleSize Factor to scale down by. + * @param subset Must be a valid subset of the original image + * dimensions and a subset supported by SkAndroidCodec. + * getSubset() can be used to obtain a subset supported + * by SkAndroidCodec. + * @return Size of the scaled partial image. Or zero size + * if either of the inputs is invalid. + */ + SkISize getSampledSubsetDimensions(int sampleSize, const SkIRect& subset) const; + + /** + * Additional options to pass to getAndroidPixels(). + */ + // FIXME: It's a bit redundant to name these AndroidOptions when this class is already + // called SkAndroidCodec. On the other hand, it's may be a bit confusing to call + // these Options when SkCodec has a slightly different set of Options. Maybe these + // should be DecodeOptions or SamplingOptions? + struct AndroidOptions : public SkCodec::Options { + AndroidOptions() + : SkCodec::Options() + , fSampleSize(1) + {} + + /** + * The client may provide an integer downscale factor for the decode. + * The codec may implement this downscaling by sampling or another + * method if it is more efficient. + * + * The default is 1, representing no downscaling. + */ + int fSampleSize; + }; + + /** + * Decode into the given pixels, a block of memory of size at + * least (info.fHeight - 1) * rowBytes + (info.fWidth * + * bytesPerPixel) + * + * Repeated calls to this function should give the same results, + * allowing the PixelRef to be immutable. + * + * @param info A description of the format (config, size) + * expected by the caller. This can simply be identical + * to the info returned by getInfo(). + * + * This contract also allows the caller to specify + * different output-configs, which the implementation can + * decide to support or not. + * + * A size that does not match getInfo() implies a request + * to scale or subset. If the codec cannot perform this + * scaling or subsetting, it will return an error code. + * + * The AndroidOptions object is also used to specify any requested scaling or subsetting + * using options->fSampleSize and options->fSubset. If NULL, the defaults (as specified above + * for AndroidOptions) are used. + * + * @return Result kSuccess, or another value explaining the type of failure. + */ + // FIXME: It's a bit redundant to name this getAndroidPixels() when this class is already + // called SkAndroidCodec. On the other hand, it's may be a bit confusing to call + // this getPixels() when it is a slightly different API than SkCodec's getPixels(). + // Maybe this should be decode() or decodeSubset()? + SkCodec::Result getAndroidPixels(const SkImageInfo& info, void* pixels, size_t rowBytes, + const AndroidOptions* options); + + /** + * Simplified version of getAndroidPixels() where we supply the default AndroidOptions as + * specified above for AndroidOptions. It will not perform any scaling or subsetting. + */ + SkCodec::Result getAndroidPixels(const SkImageInfo& info, void* pixels, size_t rowBytes); + + SkCodec::Result getPixels(const SkImageInfo& info, void* pixels, size_t rowBytes) { + return this->getAndroidPixels(info, pixels, rowBytes); + } + + SkCodec* codec() const { return fCodec.get(); } + + /** + * Retrieve the gainmap for an image. + * + * @param outInfo On success, this is populated with the parameters for + * rendering this gainmap. This parameter must be non-nullptr. + * + * @param outGainmapImageStream On success, this is populated with a stream from which the + * gainmap image may be decoded. This parameter is optional, and + * may be set to nullptr. + * + * @return If this has a gainmap image and that gainmap image was + * successfully extracted then return true. Otherwise return + * false. + */ + bool getAndroidGainmap(SkGainmapInfo* outInfo, + std::unique_ptr* outGainmapImageStream); + +protected: + SkAndroidCodec(SkCodec*); + + virtual SkISize onGetSampledDimensions(int sampleSize) const = 0; + + virtual bool onGetSupportedSubset(SkIRect* desiredSubset) const = 0; + + virtual SkCodec::Result onGetAndroidPixels(const SkImageInfo& info, void* pixels, + size_t rowBytes, const AndroidOptions& options) = 0; + +private: + const SkImageInfo fInfo; + std::unique_ptr fCodec; +}; +#endif // SkAndroidCodec_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkAvifDecoder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkAvifDecoder.h new file mode 100644 index 00000000000..840a600a317 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkAvifDecoder.h @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkAvifDecoder_DEFINED +#define SkAvifDecoder_DEFINED + +#include "include/codec/SkCodec.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +class SkData; +class SkStream; + +#include + +namespace SkAvifDecoder { + +/** Returns true if this data claims to be a AVIF image. */ +SK_API bool IsAvif(const void*, size_t); + +/** + * Attempts to decode the given bytes as a AVIF. + * + * If the bytes are not a AVIF, returns nullptr. + * + * DecodeContext is ignored + */ +SK_API std::unique_ptr Decode(std::unique_ptr, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); +SK_API std::unique_ptr Decode(sk_sp, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); + +inline constexpr SkCodecs::Decoder Decoder() { + return { "avif", IsAvif, Decode }; +} + +} // namespace SkAvifDecoder + +#endif // SkAvifDecoder_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkBmpDecoder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkBmpDecoder.h new file mode 100644 index 00000000000..104decf3cf3 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkBmpDecoder.h @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkBmpDecoder_DEFINED +#define SkBmpDecoder_DEFINED + +#include "include/codec/SkCodec.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +class SkData; +class SkStream; + +#include + +namespace SkBmpDecoder { + +/** Returns true if this data claims to be a BMP image. */ +SK_API bool IsBmp(const void*, size_t); + +/** + * Attempts to decode the given bytes as a BMP. + * + * If the bytes are not a BMP, returns nullptr. + * + * DecodeContext is ignored + */ +SK_API std::unique_ptr Decode(std::unique_ptr, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); +SK_API std::unique_ptr Decode(sk_sp, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); + +inline constexpr SkCodecs::Decoder Decoder() { + return { "bmp", IsBmp, Decode }; +} + +} // namespace SkBmpDecoder + +#endif // SkBmpDecoder_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkCodec.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkCodec.h new file mode 100644 index 00000000000..782ea69f320 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkCodec.h @@ -0,0 +1,1085 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkCodec_DEFINED +#define SkCodec_DEFINED + +#include "include/codec/SkEncodedOrigin.h" +#include "include/core/SkImageInfo.h" +#include "include/core/SkPixmap.h" +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkSize.h" +#include "include/core/SkSpan.h" +#include "include/core/SkTypes.h" +#include "include/core/SkYUVAPixmaps.h" +#include "include/private/SkEncodedInfo.h" +#include "include/private/base/SkNoncopyable.h" +#include "modules/skcms/skcms.h" + +#include +#include +#include +#include +#include +#include +#include + +class SkData; +class SkFrameHolder; +class SkImage; +class SkPngChunkReader; +class SkSampler; +class SkStream; +struct SkGainmapInfo; +enum SkAlphaType : int; +enum class SkEncodedImageFormat; + +namespace SkCodecAnimation { +enum class Blend; +enum class DisposalMethod; +} + +namespace DM { +class CodecSrc; +} // namespace DM + +namespace SkCodecs { +struct Decoder; +} + +/** + * Abstraction layer directly on top of an image codec. + */ +class SK_API SkCodec : SkNoncopyable { +public: + /** + * Minimum number of bytes that must be buffered in SkStream input. + * + * An SkStream passed to NewFromStream must be able to use this many + * bytes to determine the image type. Then the same SkStream must be + * passed to the correct decoder to read from the beginning. + * + * This can be accomplished by implementing peek() to support peeking + * this many bytes, or by implementing rewind() to be able to rewind() + * after reading this many bytes. + */ + static constexpr size_t MinBufferedBytesNeeded() { return 32; } + + /** + * Error codes for various SkCodec methods. + */ + enum Result { + /** + * General return value for success. + */ + kSuccess, + /** + * The input is incomplete. A partial image was generated. + */ + kIncompleteInput, + /** + * Like kIncompleteInput, except the input had an error. + * + * If returned from an incremental decode, decoding cannot continue, + * even with more data. + */ + kErrorInInput, + /** + * The generator cannot convert to match the request, ignoring + * dimensions. + */ + kInvalidConversion, + /** + * The generator cannot scale to requested size. + */ + kInvalidScale, + /** + * Parameters (besides info) are invalid. e.g. NULL pixels, rowBytes + * too small, etc. + */ + kInvalidParameters, + /** + * The input did not contain a valid image. + */ + kInvalidInput, + /** + * Fulfilling this request requires rewinding the input, which is not + * supported for this input. + */ + kCouldNotRewind, + /** + * An internal error, such as OOM. + */ + kInternalError, + /** + * This method is not implemented by this codec. + * FIXME: Perhaps this should be kUnsupported? + */ + kUnimplemented, + }; + + /** + * Readable string representing the error code. + */ + static const char* ResultToString(Result); + + /** + * For container formats that contain both still images and image sequences, + * instruct the decoder how the output should be selected. (Refer to comments + * for each value for more details.) + */ + enum class SelectionPolicy { + /** + * If the container format contains both still images and image sequences, + * SkCodec should choose one of the still images. This is the default. + * Note that kPreferStillImage may prevent use of the animation features + * if the input is not rewindable. + */ + kPreferStillImage, + /** + * If the container format contains both still images and image sequences, + * SkCodec should choose one of the image sequences for animation. + */ + kPreferAnimation, + }; + + /** + * If this stream represents an encoded image that we know how to decode, + * return an SkCodec that can decode it. Otherwise return NULL. + * + * As stated above, this call must be able to peek or read + * MinBufferedBytesNeeded to determine the correct format, and then start + * reading from the beginning. First it will attempt to peek, and it + * assumes that if less than MinBufferedBytesNeeded bytes (but more than + * zero) are returned, this is because the stream is shorter than this, + * so falling back to reading would not provide more data. If peek() + * returns zero bytes, this call will instead attempt to read(). This + * will require that the stream can be rewind()ed. + * + * If Result is not NULL, it will be set to either kSuccess if an SkCodec + * is returned or a reason for the failure if NULL is returned. + * + * If SkPngChunkReader is not NULL, take a ref and pass it to libpng if + * the image is a png. + * + * If the SkPngChunkReader is not NULL then: + * If the image is not a PNG, the SkPngChunkReader will be ignored. + * If the image is a PNG, the SkPngChunkReader will be reffed. + * If the PNG has unknown chunks, the SkPngChunkReader will be used + * to handle these chunks. SkPngChunkReader will be called to read + * any unknown chunk at any point during the creation of the codec + * or the decode. Note that if SkPngChunkReader fails to read a + * chunk, this could result in a failure to create the codec or a + * failure to decode the image. + * If the PNG does not contain unknown chunks, the SkPngChunkReader + * will not be used or modified. + * + * If NULL is returned, the stream is deleted immediately. Otherwise, the + * SkCodec takes ownership of it, and will delete it when done with it. + */ + static std::unique_ptr MakeFromStream( + std::unique_ptr, + SkSpan decoders, + Result* = nullptr, + SkPngChunkReader* = nullptr, + SelectionPolicy selectionPolicy = SelectionPolicy::kPreferStillImage); + // deprecated + static std::unique_ptr MakeFromStream( + std::unique_ptr, + Result* = nullptr, + SkPngChunkReader* = nullptr, + SelectionPolicy selectionPolicy = SelectionPolicy::kPreferStillImage); + + /** + * If this data represents an encoded image that we know how to decode, + * return an SkCodec that can decode it. Otherwise return NULL. + * + * If the SkPngChunkReader is not NULL then: + * If the image is not a PNG, the SkPngChunkReader will be ignored. + * If the image is a PNG, the SkPngChunkReader will be reffed. + * If the PNG has unknown chunks, the SkPngChunkReader will be used + * to handle these chunks. SkPngChunkReader will be called to read + * any unknown chunk at any point during the creation of the codec + * or the decode. Note that if SkPngChunkReader fails to read a + * chunk, this could result in a failure to create the codec or a + * failure to decode the image. + * If the PNG does not contain unknown chunks, the SkPngChunkReader + * will not be used or modified. + */ + static std::unique_ptr MakeFromData(sk_sp, + SkSpan decoders, + SkPngChunkReader* = nullptr); + // deprecated + static std::unique_ptr MakeFromData(sk_sp, SkPngChunkReader* = nullptr); + + virtual ~SkCodec(); + + /** + * Return a reasonable SkImageInfo to decode into. + * + * If the image has an ICC profile that does not map to an SkColorSpace, + * the returned SkImageInfo will use SRGB. + */ + SkImageInfo getInfo() const { return fEncodedInfo.makeImageInfo(); } + + SkISize dimensions() const { return {fEncodedInfo.width(), fEncodedInfo.height()}; } + SkIRect bounds() const { + return SkIRect::MakeWH(fEncodedInfo.width(), fEncodedInfo.height()); + } + + /** + * Return the ICC profile of the encoded data. + */ + const skcms_ICCProfile* getICCProfile() const { + return this->getEncodedInfo().profile(); + } + + /** + * Returns the image orientation stored in the EXIF data. + * If there is no EXIF data, or if we cannot read the EXIF data, returns kTopLeft. + */ + SkEncodedOrigin getOrigin() const { return fOrigin; } + + /** + * Return a size that approximately supports the desired scale factor. + * The codec may not be able to scale efficiently to the exact scale + * factor requested, so return a size that approximates that scale. + * The returned value is the codec's suggestion for the closest valid + * scale that it can natively support + */ + SkISize getScaledDimensions(float desiredScale) const { + // Negative and zero scales are errors. + SkASSERT(desiredScale > 0.0f); + if (desiredScale <= 0.0f) { + return SkISize::Make(0, 0); + } + + // Upscaling is not supported. Return the original size if the client + // requests an upscale. + if (desiredScale >= 1.0f) { + return this->dimensions(); + } + return this->onGetScaledDimensions(desiredScale); + } + + /** + * Return (via desiredSubset) a subset which can decoded from this codec, + * or false if this codec cannot decode subsets or anything similar to + * desiredSubset. + * + * @param desiredSubset In/out parameter. As input, a desired subset of + * the original bounds (as specified by getInfo). If true is returned, + * desiredSubset may have been modified to a subset which is + * supported. Although a particular change may have been made to + * desiredSubset to create something supported, it is possible other + * changes could result in a valid subset. + * If false is returned, desiredSubset's value is undefined. + * @return true if this codec supports decoding desiredSubset (as + * returned, potentially modified) + */ + bool getValidSubset(SkIRect* desiredSubset) const { + return this->onGetValidSubset(desiredSubset); + } + + /** + * Format of the encoded data. + */ + SkEncodedImageFormat getEncodedFormat() const { return this->onGetEncodedFormat(); } + + /** + * Return the underlying encoded data stream. This may be nullptr if the original + * stream could not be duplicated. + */ + virtual std::unique_ptr getEncodedData() const; + + /** + * Whether or not the memory passed to getPixels is zero initialized. + */ + enum ZeroInitialized { + /** + * The memory passed to getPixels is zero initialized. The SkCodec + * may take advantage of this by skipping writing zeroes. + */ + kYes_ZeroInitialized, + /** + * The memory passed to getPixels has not been initialized to zero, + * so the SkCodec must write all zeroes to memory. + * + * This is the default. It will be used if no Options struct is used. + */ + kNo_ZeroInitialized, + }; + + /** + * Additional options to pass to getPixels. + */ + struct Options { + Options() + : fZeroInitialized(kNo_ZeroInitialized) + , fSubset(nullptr) + , fFrameIndex(0) + , fPriorFrame(kNoFrame) + {} + + ZeroInitialized fZeroInitialized; + /** + * If not NULL, represents a subset of the original image to decode. + * Must be within the bounds returned by getInfo(). + * If the EncodedFormat is SkEncodedImageFormat::kWEBP (the only one which + * currently supports subsets), the top and left values must be even. + * + * In getPixels and incremental decode, we will attempt to decode the + * exact rectangular subset specified by fSubset. + * + * In a scanline decode, it does not make sense to specify a subset + * top or subset height, since the client already controls which rows + * to get and which rows to skip. During scanline decodes, we will + * require that the subset top be zero and the subset height be equal + * to the full height. We will, however, use the values of + * subset left and subset width to decode partial scanlines on calls + * to getScanlines(). + */ + const SkIRect* fSubset; + + /** + * The frame to decode. + * + * Only meaningful for multi-frame images. + */ + int fFrameIndex; + + /** + * If not kNoFrame, the dst already contains the prior frame at this index. + * + * Only meaningful for multi-frame images. + * + * If fFrameIndex needs to be blended with a prior frame (as reported by + * getFrameInfo[fFrameIndex].fRequiredFrame), the client can set this to + * any non-kRestorePrevious frame in [fRequiredFrame, fFrameIndex) to + * indicate that that frame is already in the dst. Options.fZeroInitialized + * is ignored in this case. + * + * If set to kNoFrame, the codec will decode any necessary required frame(s) first. + */ + int fPriorFrame; + }; + + /** + * Decode into the given pixels, a block of memory of size at + * least (info.fHeight - 1) * rowBytes + (info.fWidth * + * bytesPerPixel) + * + * Repeated calls to this function should give the same results, + * allowing the PixelRef to be immutable. + * + * @param info A description of the format (config, size) + * expected by the caller. This can simply be identical + * to the info returned by getInfo(). + * + * This contract also allows the caller to specify + * different output-configs, which the implementation can + * decide to support or not. + * + * A size that does not match getInfo() implies a request + * to scale. If the generator cannot perform this scale, + * it will return kInvalidScale. + * + * If the info contains a non-null SkColorSpace, the codec + * will perform the appropriate color space transformation. + * + * If the caller passes in the SkColorSpace that maps to the + * ICC profile reported by getICCProfile(), the color space + * transformation is a no-op. + * + * If the caller passes a null SkColorSpace, no color space + * transformation will be done. + * + * If a scanline decode is in progress, scanline mode will end, requiring the client to call + * startScanlineDecode() in order to return to decoding scanlines. + * + * @return Result kSuccess, or another value explaining the type of failure. + */ + Result getPixels(const SkImageInfo& info, void* pixels, size_t rowBytes, const Options*); + + /** + * Simplified version of getPixels() that uses the default Options. + */ + Result getPixels(const SkImageInfo& info, void* pixels, size_t rowBytes) { + return this->getPixels(info, pixels, rowBytes, nullptr); + } + + Result getPixels(const SkPixmap& pm, const Options* opts = nullptr) { + return this->getPixels(pm.info(), pm.writable_addr(), pm.rowBytes(), opts); + } + + /** + * Return an image containing the pixels. If the codec's origin is not "upper left", + * This will rotate the output image accordingly. + */ + std::tuple, SkCodec::Result> getImage(const SkImageInfo& info, + const Options* opts = nullptr); + std::tuple, SkCodec::Result> getImage(); + + /** + * If decoding to YUV is supported, this returns true. Otherwise, this + * returns false and the caller will ignore output parameter yuvaPixmapInfo. + * + * @param supportedDataTypes Indicates the data type/planar config combinations that are + * supported by the caller. If the generator supports decoding to + * YUV(A), but not as a type in supportedDataTypes, this method + * returns false. + * @param yuvaPixmapInfo Output parameter that specifies the planar configuration, subsampling, + * orientation, chroma siting, plane color types, and row bytes. + */ + bool queryYUVAInfo(const SkYUVAPixmapInfo::SupportedDataTypes& supportedDataTypes, + SkYUVAPixmapInfo* yuvaPixmapInfo) const; + + /** + * Returns kSuccess, or another value explaining the type of failure. + * This always attempts to perform a full decode. To get the planar + * configuration without decoding use queryYUVAInfo(). + * + * @param yuvaPixmaps Contains preallocated pixmaps configured according to a successful call + * to queryYUVAInfo(). + */ + Result getYUVAPlanes(const SkYUVAPixmaps& yuvaPixmaps); + + /** + * Prepare for an incremental decode with the specified options. + * + * This may require a rewind. + * + * If kIncompleteInput is returned, may be called again after more data has + * been provided to the source SkStream. + * + * @param dstInfo Info of the destination. If the dimensions do not match + * those of getInfo, this implies a scale. + * @param dst Memory to write to. Needs to be large enough to hold the subset, + * if present, or the full image as described in dstInfo. + * @param options Contains decoding options, including if memory is zero + * initialized and whether to decode a subset. + * @return Enum representing success or reason for failure. + */ + Result startIncrementalDecode(const SkImageInfo& dstInfo, void* dst, size_t rowBytes, + const Options*); + + Result startIncrementalDecode(const SkImageInfo& dstInfo, void* dst, size_t rowBytes) { + return this->startIncrementalDecode(dstInfo, dst, rowBytes, nullptr); + } + + /** + * Start/continue the incremental decode. + * + * Not valid to call before a call to startIncrementalDecode() returns + * kSuccess. + * + * If kIncompleteInput is returned, may be called again after more data has + * been provided to the source SkStream. + * + * Unlike getPixels and getScanlines, this does not do any filling. This is + * left up to the caller, since they may be skipping lines or continuing the + * decode later. In the latter case, they may choose to initialize all lines + * first, or only initialize the remaining lines after the first call. + * + * @param rowsDecoded Optional output variable returning the total number of + * lines initialized. Only meaningful if this method returns kIncompleteInput. + * Otherwise the implementation may not set it. + * Note that some implementations may have initialized this many rows, but + * not necessarily finished those rows (e.g. interlaced PNG). This may be + * useful for determining what rows the client needs to initialize. + * @return kSuccess if all lines requested in startIncrementalDecode have + * been completely decoded. kIncompleteInput otherwise. + */ + Result incrementalDecode(int* rowsDecoded = nullptr) { + if (!fStartedIncrementalDecode) { + return kInvalidParameters; + } + return this->onIncrementalDecode(rowsDecoded); + } + + /** + * The remaining functions revolve around decoding scanlines. + */ + + /** + * Prepare for a scanline decode with the specified options. + * + * After this call, this class will be ready to decode the first scanline. + * + * This must be called in order to call getScanlines or skipScanlines. + * + * This may require rewinding the stream. + * + * Not all SkCodecs support this. + * + * @param dstInfo Info of the destination. If the dimensions do not match + * those of getInfo, this implies a scale. + * @param options Contains decoding options, including if memory is zero + * initialized. + * @return Enum representing success or reason for failure. + */ + Result startScanlineDecode(const SkImageInfo& dstInfo, const Options* options); + + /** + * Simplified version of startScanlineDecode() that uses the default Options. + */ + Result startScanlineDecode(const SkImageInfo& dstInfo) { + return this->startScanlineDecode(dstInfo, nullptr); + } + + /** + * Write the next countLines scanlines into dst. + * + * Not valid to call before calling startScanlineDecode(). + * + * @param dst Must be non-null, and large enough to hold countLines + * scanlines of size rowBytes. + * @param countLines Number of lines to write. + * @param rowBytes Number of bytes per row. Must be large enough to hold + * a scanline based on the SkImageInfo used to create this object. + * @return the number of lines successfully decoded. If this value is + * less than countLines, this will fill the remaining lines with a + * default value. + */ + int getScanlines(void* dst, int countLines, size_t rowBytes); + + /** + * Skip count scanlines. + * + * Not valid to call before calling startScanlineDecode(). + * + * The default version just calls onGetScanlines and discards the dst. + * NOTE: If skipped lines are the only lines with alpha, this default + * will make reallyHasAlpha return true, when it could have returned + * false. + * + * @return true if the scanlines were successfully skipped + * false on failure, possible reasons for failure include: + * An incomplete input image stream. + * Calling this function before calling startScanlineDecode(). + * If countLines is less than zero or so large that it moves + * the current scanline past the end of the image. + */ + bool skipScanlines(int countLines); + + /** + * The order in which rows are output from the scanline decoder is not the + * same for all variations of all image types. This explains the possible + * output row orderings. + */ + enum SkScanlineOrder { + /* + * By far the most common, this indicates that the image can be decoded + * reliably using the scanline decoder, and that rows will be output in + * the logical order. + */ + kTopDown_SkScanlineOrder, + + /* + * This indicates that the scanline decoder reliably outputs rows, but + * they will be returned in reverse order. If the scanline format is + * kBottomUp, the nextScanline() API can be used to determine the actual + * y-coordinate of the next output row, but the client is not forced + * to take advantage of this, given that it's not too tough to keep + * track independently. + * + * For full image decodes, it is safe to get all of the scanlines at + * once, since the decoder will handle inverting the rows as it + * decodes. + * + * For subset decodes and sampling, it is simplest to get and skip + * scanlines one at a time, using the nextScanline() API. It is + * possible to ask for larger chunks at a time, but this should be used + * with caution. As with full image decodes, the decoder will handle + * inverting the requested rows, but rows will still be delivered + * starting from the bottom of the image. + * + * Upside down bmps are an example. + */ + kBottomUp_SkScanlineOrder, + }; + + /** + * An enum representing the order in which scanlines will be returned by + * the scanline decoder. + * + * This is undefined before startScanlineDecode() is called. + */ + SkScanlineOrder getScanlineOrder() const { return this->onGetScanlineOrder(); } + + /** + * Returns the y-coordinate of the next row to be returned by the scanline + * decoder. + * + * This will equal fCurrScanline, except in the case of strangely + * encoded image types (bottom-up bmps). + * + * Results are undefined when not in scanline decoding mode. + */ + int nextScanline() const { return this->outputScanline(fCurrScanline); } + + /** + * Returns the output y-coordinate of the row that corresponds to an input + * y-coordinate. The input y-coordinate represents where the scanline + * is located in the encoded data. + * + * This will equal inputScanline, except in the case of strangely + * encoded image types (bottom-up bmps, interlaced gifs). + */ + int outputScanline(int inputScanline) const; + + /** + * Return the number of frames in the image. + * + * May require reading through the stream. + */ + int getFrameCount() { + return this->onGetFrameCount(); + } + + // Sentinel value used when a frame index implies "no frame": + // - FrameInfo::fRequiredFrame set to this value means the frame + // is independent. + // - Options::fPriorFrame set to this value means no (relevant) prior frame + // is residing in dst's memory. + static constexpr int kNoFrame = -1; + + // This transitional definition was added in August 2018, and will eventually be removed. +#ifdef SK_LEGACY_SKCODEC_NONE_ENUM + static constexpr int kNone = kNoFrame; +#endif + + /** + * Information about individual frames in a multi-framed image. + */ + struct FrameInfo { + /** + * The frame that this frame needs to be blended with, or + * kNoFrame if this frame is independent (so it can be + * drawn over an uninitialized buffer). + * + * Note that this is the *earliest* frame that can be used + * for blending. Any frame from [fRequiredFrame, i) can be + * used, unless its fDisposalMethod is kRestorePrevious. + */ + int fRequiredFrame; + + /** + * Number of milliseconds to show this frame. + */ + int fDuration; + + /** + * Whether the end marker for this frame is contained in the stream. + * + * Note: this does not guarantee that an attempt to decode will be complete. + * There could be an error in the stream. + */ + bool fFullyReceived; + + /** + * This is conservative; it will still return non-opaque if e.g. a + * color index-based frame has a color with alpha but does not use it. + */ + SkAlphaType fAlphaType; + + /** + * Whether the updated rectangle contains alpha. + * + * This is conservative; it will still be set to true if e.g. a color + * index-based frame has a color with alpha but does not use it. In + * addition, it may be set to true, even if the final frame, after + * blending, is opaque. + */ + bool fHasAlphaWithinBounds; + + /** + * How this frame should be modified before decoding the next one. + */ + SkCodecAnimation::DisposalMethod fDisposalMethod; + + /** + * How this frame should blend with the prior frame. + */ + SkCodecAnimation::Blend fBlend; + + /** + * The rectangle updated by this frame. + * + * It may be empty, if the frame does not change the image. It will + * always be contained by SkCodec::dimensions(). + */ + SkIRect fFrameRect; + }; + + /** + * Return info about a single frame. + * + * Does not read through the stream, so it should be called after + * getFrameCount() to parse any frames that have not already been parsed. + * + * Only supported by animated (multi-frame) codecs. Note that this is a + * property of the codec (the SkCodec subclass), not the image. + * + * To elaborate, some codecs support animation (e.g. GIF). Others do not + * (e.g. BMP). Animated codecs can still represent single frame images. + * Calling getFrameInfo(0, etc) will return true for a single frame GIF + * even if the overall image is not animated (in that the pixels on screen + * do not change over time). When incrementally decoding a GIF image, we + * might only know that there's a single frame *so far*. + * + * For non-animated SkCodec subclasses, it's sufficient but not necessary + * for this method to always return false. + */ + bool getFrameInfo(int index, FrameInfo* info) const { + if (index < 0) { + return false; + } + return this->onGetFrameInfo(index, info); + } + + /** + * Return info about all the frames in the image. + * + * May require reading through the stream to determine info about the + * frames (including the count). + * + * As such, future decoding calls may require a rewind. + * + * This may return an empty vector for non-animated codecs. See the + * getFrameInfo(int, FrameInfo*) comment. + */ + std::vector getFrameInfo(); + + static constexpr int kRepetitionCountInfinite = -1; + + /** + * Return the number of times to repeat, if this image is animated. This number does not + * include the first play through of each frame. For example, a repetition count of 4 means + * that each frame is played 5 times and then the animation stops. + * + * It can return kRepetitionCountInfinite, a negative number, meaning that the animation + * should loop forever. + * + * May require reading the stream to find the repetition count. + * + * As such, future decoding calls may require a rewind. + * + * For still (non-animated) image codecs, this will return 0. + */ + int getRepetitionCount() { + return this->onGetRepetitionCount(); + } + + // Register a decoder at runtime by passing two function pointers: + // - peek() to return true if the span of bytes appears to be your encoded format; + // - make() to attempt to create an SkCodec from the given stream. + // Not thread safe. + static void Register( + bool (*peek)(const void*, size_t), + std::unique_ptr (*make)(std::unique_ptr, SkCodec::Result*)); + +protected: + const SkEncodedInfo& getEncodedInfo() const { return fEncodedInfo; } + + using XformFormat = skcms_PixelFormat; + + SkCodec(SkEncodedInfo&&, + XformFormat srcFormat, + std::unique_ptr, + SkEncodedOrigin = kTopLeft_SkEncodedOrigin); + + void setSrcXformFormat(XformFormat pixelFormat); + + XformFormat getSrcXformFormat() const { + return fSrcXformFormat; + } + + virtual bool onGetGainmapInfo(SkGainmapInfo*, std::unique_ptr*) { return false; } + + virtual SkISize onGetScaledDimensions(float /*desiredScale*/) const { + // By default, scaling is not supported. + return this->dimensions(); + } + + // FIXME: What to do about subsets?? + /** + * Subclasses should override if they support dimensions other than the + * srcInfo's. + */ + virtual bool onDimensionsSupported(const SkISize&) { + return false; + } + + virtual SkEncodedImageFormat onGetEncodedFormat() const = 0; + + /** + * @param rowsDecoded When the encoded image stream is incomplete, this function + * will return kIncompleteInput and rowsDecoded will be set to + * the number of scanlines that were successfully decoded. + * This will allow getPixels() to fill the uninitialized memory. + */ + virtual Result onGetPixels(const SkImageInfo& info, + void* pixels, size_t rowBytes, const Options&, + int* rowsDecoded) = 0; + + virtual bool onQueryYUVAInfo(const SkYUVAPixmapInfo::SupportedDataTypes&, + SkYUVAPixmapInfo*) const { return false; } + + virtual Result onGetYUVAPlanes(const SkYUVAPixmaps&) { return kUnimplemented; } + + virtual bool onGetValidSubset(SkIRect* /*desiredSubset*/) const { + // By default, subsets are not supported. + return false; + } + + /** + * If the stream was previously read, attempt to rewind. + * + * If the stream needed to be rewound, call onRewind. + * @returns true if the codec is at the right position and can be used. + * false if there was a failure to rewind. + * + * This is called by getPixels(), getYUV8Planes(), startIncrementalDecode() and + * startScanlineDecode(). Subclasses may call if they need to rewind at another time. + */ + [[nodiscard]] bool rewindIfNeeded(); + + /** + * Called by rewindIfNeeded, if the stream needed to be rewound. + * + * Subclasses should do any set up needed after a rewind. + */ + virtual bool onRewind() { + return true; + } + + /** + * Get method for the input stream + */ + SkStream* stream() { + return fStream.get(); + } + + /** + * The remaining functions revolve around decoding scanlines. + */ + + /** + * Most images types will be kTopDown and will not need to override this function. + */ + virtual SkScanlineOrder onGetScanlineOrder() const { return kTopDown_SkScanlineOrder; } + + const SkImageInfo& dstInfo() const { return fDstInfo; } + + const Options& options() const { return fOptions; } + + /** + * Returns the number of scanlines that have been decoded so far. + * This is unaffected by the SkScanlineOrder. + * + * Returns -1 if we have not started a scanline decode. + */ + int currScanline() const { return fCurrScanline; } + + virtual int onOutputScanline(int inputScanline) const; + + /** + * Return whether we can convert to dst. + * + * Will be called for the appropriate frame, prior to initializing the colorXform. + */ + virtual bool conversionSupported(const SkImageInfo& dst, bool srcIsOpaque, + bool needsColorXform); + + // Some classes never need a colorXform e.g. + // - ICO uses its embedded codec's colorXform + // - WBMP is just Black/White + virtual bool usesColorXform() const { return true; } + void applyColorXform(void* dst, const void* src, int count) const; + + bool colorXform() const { return fXformTime != kNo_XformTime; } + bool xformOnDecode() const { return fXformTime == kDecodeRow_XformTime; } + + virtual int onGetFrameCount() { + return 1; + } + + virtual bool onGetFrameInfo(int, FrameInfo*) const { + return false; + } + + virtual int onGetRepetitionCount() { + return 0; + } + +private: + const SkEncodedInfo fEncodedInfo; + XformFormat fSrcXformFormat; + std::unique_ptr fStream; + bool fNeedsRewind = false; + const SkEncodedOrigin fOrigin; + + SkImageInfo fDstInfo; + Options fOptions; + + enum XformTime { + kNo_XformTime, + kPalette_XformTime, + kDecodeRow_XformTime, + }; + XformTime fXformTime; + XformFormat fDstXformFormat; // Based on fDstInfo. + skcms_ICCProfile fDstProfile; + skcms_AlphaFormat fDstXformAlphaFormat; + + // Only meaningful during scanline decodes. + int fCurrScanline = -1; + + bool fStartedIncrementalDecode = false; + + // Allows SkAndroidCodec to call handleFrameIndex (potentially decoding a prior frame and + // clearing to transparent) without SkCodec itself calling it, too. + bool fUsingCallbackForHandleFrameIndex = false; + + bool initializeColorXform(const SkImageInfo& dstInfo, SkEncodedInfo::Alpha, bool srcIsOpaque); + + /** + * Return whether these dimensions are supported as a scale. + * + * The codec may choose to cache the information about scale and subset. + * Either way, the same information will be passed to onGetPixels/onStart + * on success. + * + * This must return true for a size returned from getScaledDimensions. + */ + bool dimensionsSupported(const SkISize& dim) { + return dim == this->dimensions() || this->onDimensionsSupported(dim); + } + + /** + * For multi-framed images, return the object with information about the frames. + */ + virtual const SkFrameHolder* getFrameHolder() const { + return nullptr; + } + + // Callback for decoding a prior frame. The `Options::fFrameIndex` is ignored, + // being replaced by frameIndex. This allows opts to actually be a subclass of + // SkCodec::Options which SkCodec itself does not know how to copy or modify, + // but just passes through to the caller (where it can be reinterpret_cast'd). + using GetPixelsCallback = std::function; + + /** + * Check for a valid Options.fFrameIndex, and decode prior frames if necessary. + * + * If GetPixelsCallback is not null, it will be used to decode a prior frame instead + * of using this SkCodec directly. It may also be used recursively, if that in turn + * depends on a prior frame. This is used by SkAndroidCodec. + */ + Result handleFrameIndex(const SkImageInfo&, void* pixels, size_t rowBytes, const Options&, + GetPixelsCallback = nullptr); + + // Methods for scanline decoding. + virtual Result onStartScanlineDecode(const SkImageInfo& /*dstInfo*/, + const Options& /*options*/) { + return kUnimplemented; + } + + virtual Result onStartIncrementalDecode(const SkImageInfo& /*dstInfo*/, void*, size_t, + const Options&) { + return kUnimplemented; + } + + virtual Result onIncrementalDecode(int*) { + return kUnimplemented; + } + + + virtual bool onSkipScanlines(int /*countLines*/) { return false; } + + virtual int onGetScanlines(void* /*dst*/, int /*countLines*/, size_t /*rowBytes*/) { return 0; } + + /** + * On an incomplete decode, getPixels() and getScanlines() will call this function + * to fill any uinitialized memory. + * + * @param dstInfo Contains the destination color type + * Contains the destination alpha type + * Contains the destination width + * The height stored in this info is unused + * @param dst Pointer to the start of destination pixel memory + * @param rowBytes Stride length in destination pixel memory + * @param zeroInit Indicates if memory is zero initialized + * @param linesRequested Number of lines that the client requested + * @param linesDecoded Number of lines that were successfully decoded + */ + void fillIncompleteImage(const SkImageInfo& dstInfo, void* dst, size_t rowBytes, + ZeroInitialized zeroInit, int linesRequested, int linesDecoded); + + /** + * Return an object which will allow forcing scanline decodes to sample in X. + * + * May create a sampler, if one is not currently being used. Otherwise, does + * not affect ownership. + * + * Only valid during scanline decoding or incremental decoding. + */ + virtual SkSampler* getSampler(bool /*createIfNecessary*/) { return nullptr; } + + friend class DM::CodecSrc; // for fillIncompleteImage + friend class PNGCodecGM; // for fillIncompleteImage + friend class SkSampledCodec; + friend class SkIcoCodec; + friend class SkAndroidCodec; // for fEncodedInfo + friend class SkPDFBitmap; // for fEncodedInfo +}; + +namespace SkCodecs { + +using DecodeContext = void*; +using IsFormatCallback = bool (*)(const void* data, size_t len); +using MakeFromStreamCallback = std::unique_ptr (*)(std::unique_ptr, + SkCodec::Result*, + DecodeContext); + +struct SK_API Decoder { + // By convention, we use all lowercase letters and go with the primary filename extension. + // For example "png", "jpg", "ico", "webp", etc + std::string_view id; + IsFormatCallback isFormat; + MakeFromStreamCallback makeFromStream; +}; + +// Add the decoder to the end of a linked list of decoders, which will be used to identify calls to +// SkCodec::MakeFromStream. If a decoder with the same id already exists, this new decoder +// will replace the existing one (in the same position). This is not thread-safe, so make sure all +// initialization is done before the first call. +void SK_API Register(Decoder d); + +/** + * Return a SkImage produced by the codec, but attempts to defer image allocation until the + * image is actually used/drawn. This deferral allows the system to cache the result, either on the + * CPU or on the GPU, depending on where the image is drawn. If memory is low, the cache may + * be purged, causing the next draw of the image to have to re-decode. + * + * If alphaType is nullopt, the image's alpha type will be chosen automatically based on the + * image format. Transparent images will default to kPremul_SkAlphaType. If alphaType contains + * kPremul_SkAlphaType or kUnpremul_SkAlphaType, that alpha type will be used. Forcing opaque + * (passing kOpaque_SkAlphaType) is not allowed, and will return nullptr. + * + * @param codec A non-null codec (e.g. from SkPngDecoder::Decode) + * @return created SkImage, or nullptr + */ +SK_API sk_sp DeferredImage(std::unique_ptr codec, + std::optional alphaType = std::nullopt); +} + +#endif // SkCodec_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkCodecAnimation.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkCodecAnimation.h new file mode 100644 index 00000000000..c5883e2af2c --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkCodecAnimation.h @@ -0,0 +1,61 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkCodecAnimation_DEFINED +#define SkCodecAnimation_DEFINED + +namespace SkCodecAnimation { + /** + * This specifies how the next frame is based on this frame. + * + * Names are based on the GIF 89a spec. + * + * The numbers correspond to values in a GIF. + */ + enum class DisposalMethod { + /** + * The next frame should be drawn on top of this one. + * + * In a GIF, a value of 0 (not specified) is also treated as Keep. + */ + kKeep = 1, + + /** + * Similar to Keep, except the area inside this frame's rectangle + * should be cleared to the BackGround color (transparent) before + * drawing the next frame. + */ + kRestoreBGColor = 2, + + /** + * The next frame should be drawn on top of the previous frame - i.e. + * disregarding this one. + * + * In a GIF, a value of 4 is also treated as RestorePrevious. + */ + kRestorePrevious = 3, + }; + + /** + * How to blend the current frame. + */ + enum class Blend { + /** + * Blend with the prior frame as if using SkBlendMode::kSrcOver. + */ + kSrcOver, + + /** + * Blend with the prior frame as if using SkBlendMode::kSrc. + * + * This frame's pixels replace the destination pixels. + */ + kSrc, + }; + +} // namespace SkCodecAnimation +#endif // SkCodecAnimation_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkEncodedImageFormat.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkEncodedImageFormat.h new file mode 100644 index 00000000000..e664c7db02b --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkEncodedImageFormat.h @@ -0,0 +1,33 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkEncodedImageFormat_DEFINED +#define SkEncodedImageFormat_DEFINED + +#include + +/** + * Enum describing format of encoded data. + */ +enum class SkEncodedImageFormat { + kBMP, + kGIF, + kICO, + kJPEG, + kPNG, + kWBMP, + kWEBP, + kPKM, + kKTX, + kASTC, + kDNG, + kHEIF, + kAVIF, + kJPEGXL, +}; + +#endif // SkEncodedImageFormat_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkEncodedOrigin.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkEncodedOrigin.h new file mode 100644 index 00000000000..19d083672f6 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkEncodedOrigin.h @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkEncodedOrigin_DEFINED +#define SkEncodedOrigin_DEFINED + +#include "include/core/SkMatrix.h" + +// These values match the orientation www.exif.org/Exif2-2.PDF. +enum SkEncodedOrigin { + kTopLeft_SkEncodedOrigin = 1, // Default + kTopRight_SkEncodedOrigin = 2, // Reflected across y-axis + kBottomRight_SkEncodedOrigin = 3, // Rotated 180 + kBottomLeft_SkEncodedOrigin = 4, // Reflected across x-axis + kLeftTop_SkEncodedOrigin = 5, // Reflected across x-axis, Rotated 90 CCW + kRightTop_SkEncodedOrigin = 6, // Rotated 90 CW + kRightBottom_SkEncodedOrigin = 7, // Reflected across x-axis, Rotated 90 CW + kLeftBottom_SkEncodedOrigin = 8, // Rotated 90 CCW + kDefault_SkEncodedOrigin = kTopLeft_SkEncodedOrigin, + kLast_SkEncodedOrigin = kLeftBottom_SkEncodedOrigin, +}; + +/** + * Given an encoded origin and the width and height of the source data, returns a matrix + * that transforms the source rectangle with upper left corner at [0, 0] and origin to a correctly + * oriented destination rectangle of [0, 0, w, h]. + */ +static inline SkMatrix SkEncodedOriginToMatrix(SkEncodedOrigin origin, int w, int h) { + switch (origin) { + case kTopLeft_SkEncodedOrigin: return SkMatrix::I(); + case kTopRight_SkEncodedOrigin: return SkMatrix::MakeAll(-1, 0, w, 0, 1, 0, 0, 0, 1); + case kBottomRight_SkEncodedOrigin: return SkMatrix::MakeAll(-1, 0, w, 0, -1, h, 0, 0, 1); + case kBottomLeft_SkEncodedOrigin: return SkMatrix::MakeAll( 1, 0, 0, 0, -1, h, 0, 0, 1); + case kLeftTop_SkEncodedOrigin: return SkMatrix::MakeAll( 0, 1, 0, 1, 0, 0, 0, 0, 1); + case kRightTop_SkEncodedOrigin: return SkMatrix::MakeAll( 0, -1, w, 1, 0, 0, 0, 0, 1); + case kRightBottom_SkEncodedOrigin: return SkMatrix::MakeAll( 0, -1, w, -1, 0, h, 0, 0, 1); + case kLeftBottom_SkEncodedOrigin: return SkMatrix::MakeAll( 0, 1, 0, -1, 0, h, 0, 0, 1); + } + SK_ABORT("Unexpected origin"); +} + +/** + * Return true if the encoded origin includes a 90 degree rotation, in which case the width + * and height of the source data are swapped relative to a correctly oriented destination. + */ +static inline bool SkEncodedOriginSwapsWidthHeight(SkEncodedOrigin origin) { + return origin >= kLeftTop_SkEncodedOrigin; +} + +#endif // SkEncodedOrigin_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkGifDecoder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkGifDecoder.h new file mode 100644 index 00000000000..7344b266479 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkGifDecoder.h @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkGifDecoder_DEFINED +#define SkGifDecoder_DEFINED + +#include "include/codec/SkCodec.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +class SkData; +class SkStream; + +#include + +namespace SkGifDecoder { + +/** Returns true if this data claims to be a GIF image. */ +SK_API bool IsGif(const void*, size_t); + +/** + * Attempts to decode the given bytes as a GIF. + * + * If the bytes are not a GIF, returns nullptr. + * + * DecodeContext is ignored + */ +SK_API std::unique_ptr Decode(std::unique_ptr, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); +SK_API std::unique_ptr Decode(sk_sp, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); + +inline constexpr SkCodecs::Decoder Decoder() { + return { "gif", IsGif, Decode }; +} + +} // namespace SkGifDecoder + +#endif // SkGifDecoder_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkIcoDecoder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkIcoDecoder.h new file mode 100644 index 00000000000..e0d361d685c --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkIcoDecoder.h @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkIcoDecoder_DEFINED +#define SkIcoDecoder_DEFINED + +#include "include/codec/SkCodec.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +class SkData; +class SkStream; + +#include + +namespace SkIcoDecoder { + +/** Returns true if this data claims to be a ICO image. */ +SK_API bool IsIco(const void*, size_t); + +/** + * Attempts to decode the given bytes as a ICO. + * + * If the bytes are not a ICO, returns nullptr. + * + * DecodeContext is ignored + */ +SK_API std::unique_ptr Decode(std::unique_ptr, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); +SK_API std::unique_ptr Decode(sk_sp, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); + +inline constexpr SkCodecs::Decoder Decoder() { + return { "ico", IsIco, Decode }; +} + +} // namespace SkIcoDecoder + +#endif // SkIcoDecoder_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkJpegDecoder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkJpegDecoder.h new file mode 100644 index 00000000000..10a340f5bf2 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkJpegDecoder.h @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkJpegDecoder_DEFINED +#define SkJpegDecoder_DEFINED + +#include "include/codec/SkCodec.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +class SkData; +class SkStream; + +#include + +namespace SkJpegDecoder { + +/** Returns true if this data claims to be a JPEG image. */ +SK_API bool IsJpeg(const void*, size_t); + +/** + * Attempts to decode the given bytes as a JPEG. + * + * If the bytes are not a JPEG, returns nullptr. + * + * DecodeContext is ignored + */ +SK_API std::unique_ptr Decode(std::unique_ptr, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); +SK_API std::unique_ptr Decode(sk_sp, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); + +inline constexpr SkCodecs::Decoder Decoder() { + return { "jpeg", IsJpeg, Decode }; +} + +} // namespace SkJpegDecoder + +#endif // SkJpegDecoder_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkJpegxlDecoder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkJpegxlDecoder.h new file mode 100644 index 00000000000..4ab73f82708 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkJpegxlDecoder.h @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkJpegxlDecoder_DEFINED +#define SkJpegxlDecoder_DEFINED + +#include "include/codec/SkCodec.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +class SkData; +class SkStream; + +#include + +namespace SkJpegxlDecoder { + +/** Returns true if this data claims to be a JPEGXL image. */ +SK_API bool IsJpegxl(const void*, size_t); + +/** + * Attempts to decode the given bytes as a JPEGXL. + * + * If the bytes are not a JPEGXL, returns nullptr. + * + * DecodeContext is ignored + */ +SK_API std::unique_ptr Decode(std::unique_ptr, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); +SK_API std::unique_ptr Decode(sk_sp, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); + +inline constexpr SkCodecs::Decoder Decoder() { + return { "jpegxl", IsJpegxl, Decode }; +} + +} // namespace SkJpegxlDecoder + +#endif // SkJpegxlDecoder_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkPixmapUtils.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkPixmapUtils.h new file mode 100644 index 00000000000..0df4a36f0c2 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkPixmapUtils.h @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPixmapUtils_DEFINED +#define SkPixmapUtils_DEFINED + +#include "include/codec/SkEncodedOrigin.h" +#include "include/core/SkImageInfo.h" +#include "include/private/base/SkAPI.h" + +class SkPixmap; + +namespace SkPixmapUtils { +/** + * Copy the pixels in src into dst, applying the orientation transformations specified + * by origin. If the inputs are invalid, this returns false and no copy is made. + */ +SK_API bool Orient(const SkPixmap& dst, const SkPixmap& src, SkEncodedOrigin origin); + +/** + * Return a copy of the provided ImageInfo with the width and height swapped. + */ +SK_API SkImageInfo SwapWidthHeight(const SkImageInfo& info); + +} // namespace SkPixmapUtils + +#endif // SkPixmapUtils_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkPngChunkReader.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkPngChunkReader.h new file mode 100644 index 00000000000..0ee8a9ecc72 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkPngChunkReader.h @@ -0,0 +1,45 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPngChunkReader_DEFINED +#define SkPngChunkReader_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" + +/** + * SkPngChunkReader + * + * Base class for optional callbacks to retrieve meta/chunk data out of a PNG + * encoded image as it is being decoded. + * Used by SkCodec. + */ +class SkPngChunkReader : public SkRefCnt { +public: + /** + * This will be called by the decoder when it sees an unknown chunk. + * + * Use by SkCodec: + * Depending on the location of the unknown chunks, this callback may be + * called by + * - the factory (NewFromStream/NewFromData) + * - getPixels + * - startScanlineDecode + * - the first call to getScanlines/skipScanlines + * The callback may be called from a different thread (e.g. if the SkCodec + * is passed to another thread), and it may be called multiple times, if + * the SkCodec is used multiple times. + * + * @param tag Name for this type of chunk. + * @param data Data to be interpreted by the subclass. + * @param length Number of bytes of data in the chunk. + * @return true to continue decoding, or false to indicate an error, which + * will cause the decoder to not return the image. + */ + virtual bool readChunk(const char tag[], const void* data, size_t length) = 0; +}; +#endif // SkPngChunkReader_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkPngDecoder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkPngDecoder.h new file mode 100644 index 00000000000..e761d2e2166 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkPngDecoder.h @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkPngDecoder_DEFINED +#define SkPngDecoder_DEFINED + +#include "include/codec/SkCodec.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +class SkData; +class SkStream; + +#include + +namespace SkPngDecoder { + +/** Returns true if this data claims to be a PNG image. */ +SK_API bool IsPng(const void*, size_t); + +/** + * Attempts to decode the given bytes as a PNG. + * + * If the bytes are not a PNG, returns nullptr. + * + * DecodeContext, if non-null, is expected to be a SkPngChunkReader* + */ +SK_API std::unique_ptr Decode(std::unique_ptr, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); +SK_API std::unique_ptr Decode(sk_sp, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); + +inline constexpr SkCodecs::Decoder Decoder() { + return { "png", IsPng, Decode }; +} + +} // namespace SkPngDecoder + +#endif // SkPngDecoder_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkRawDecoder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkRawDecoder.h new file mode 100644 index 00000000000..3f56012212e --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkRawDecoder.h @@ -0,0 +1,50 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkRawDecoder_DEFINED +#define SkRawDecoder_DEFINED + +#include "include/codec/SkCodec.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +class SkData; +class SkStream; + +#include + +namespace SkRawDecoder { + +inline bool IsRaw(const void*, size_t) { + // Raw formats are tricky to detect just by reading in the first several bytes. + // For example, PIEX might need to read 10k bytes to detect Sony's arw format + // https://github.com/google/piex/blob/f1e15dd837c04347504149f71db67a78fbeddc73/src/image_type_recognition/image_type_recognition_lite.cc#L152 + // Thus, we just assume everything might be a RAW file and check it last. + return true; +} + +/** + * Attempts to decode the given bytes as a raw image. + * + * If the bytes are not a raw, returns nullptr. + * + * DecodeContext is ignored + */ +SK_API std::unique_ptr Decode(std::unique_ptr, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); +SK_API std::unique_ptr Decode(sk_sp, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); + +// This decoder will always be checked last, no matter when it is registered. +inline constexpr SkCodecs::Decoder Decoder() { + return { "raw", IsRaw, Decode }; +} + +} // namespace SkRawDecoder + +#endif // SkRawDecoder_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkWbmpDecoder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkWbmpDecoder.h new file mode 100644 index 00000000000..7e5e7706df7 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkWbmpDecoder.h @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkWbmpDecoder_DEFINED +#define SkWbmpDecoder_DEFINED + +#include "include/codec/SkCodec.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +class SkData; +class SkStream; + +#include + +namespace SkWbmpDecoder { + +/** Returns true if this data claims to be a WBMP image. */ +SK_API bool IsWbmp(const void*, size_t); + +/** + * Attempts to decode the given bytes as a WBMP. + * + * If the bytes are not a WBMP, returns nullptr. + * + * DecodeContext is ignored + */ +SK_API std::unique_ptr Decode(std::unique_ptr, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); +SK_API std::unique_ptr Decode(sk_sp, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); + +inline constexpr SkCodecs::Decoder Decoder() { + return { "wbmp", IsWbmp, Decode }; +} + +} // namespace SkWbmpDecoder + +#endif // SkWbmpDecoder_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkWebpDecoder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkWebpDecoder.h new file mode 100644 index 00000000000..5f8032f0fe4 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/codec/SkWebpDecoder.h @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkWebpDecoder_DEFINED +#define SkWebpDecoder_DEFINED + +#include "include/codec/SkCodec.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +class SkData; +class SkStream; + +#include + +namespace SkWebpDecoder { + +/** Returns true if this data claims to be a WEBP image. */ +SK_API bool IsWebp(const void*, size_t); + +/** + * Attempts to decode the given bytes as a WEBP. + * + * If the bytes are not a WEBP, returns nullptr. + * + * DecodeContext is ignored + */ +SK_API std::unique_ptr Decode(std::unique_ptr, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); +SK_API std::unique_ptr Decode(sk_sp, + SkCodec::Result*, + SkCodecs::DecodeContext = nullptr); + +inline constexpr SkCodecs::Decoder Decoder() { + return { "webp", IsWebp, Decode }; +} + +} // namespace SkWebpDecoder + +#endif // SkWebpDecoder_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/config/OWNERS b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/config/OWNERS new file mode 100644 index 00000000000..25b714bb273 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/config/OWNERS @@ -0,0 +1,2 @@ +bungeman@google.com +kjlubick@google.com \ No newline at end of file diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/config/SkUserConfig.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/config/SkUserConfig.h new file mode 100644 index 00000000000..8ac155b4814 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/config/SkUserConfig.h @@ -0,0 +1,121 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkUserConfig_DEFINED +#define SkUserConfig_DEFINED + +/* SkTypes.h, the root of the public header files, includes this file + SkUserConfig.h after first initializing certain Skia defines, letting + this file change or augment those flags. + + Below are optional defines that add, subtract, or change default behavior + in Skia. Your port can locally edit this file to enable/disable flags as + you choose, or these can be declared on your command line (i.e. -Dfoo). + + By default, this #include file will always default to having all the flags + commented out, so including it will have no effect. +*/ + +/////////////////////////////////////////////////////////////////////////////// + +/* Skia has lots of debug-only code. Often this is just null checks or other + parameter checking, but sometimes it can be quite intrusive (e.g. check that + each 32bit pixel is in premultiplied form). This code can be very useful + during development, but will slow things down in a shipping product. + + By default, these mutually exclusive flags are defined in SkTypes.h, + based on the presence or absence of NDEBUG, but that decision can be changed + here. +*/ +//#define SK_DEBUG +//#define SK_RELEASE + +/* To write debug messages to a console, skia will call SkDebugf(...) following + printf conventions (e.g. const char* format, ...). If you want to redirect + this to something other than printf, define yours here +*/ +//#define SkDebugf(...) MyFunction(__VA_ARGS__) + +/* Skia has both debug and release asserts. When an assert fails SK_ABORT will + be used to report an abort message. SK_ABORT is expected not to return. Skia + provides a default implementation which will print the message with SkDebugf + and then call sk_abort_no_print. +*/ +//#define SK_ABORT(message, ...) + +/* To specify a different default font strike cache memory limit, define this. If this is + undefined, skia will use a built-in value. +*/ +//#define SK_DEFAULT_FONT_CACHE_LIMIT (1024 * 1024) + +/* To specify a different default font strike cache count limit, define this. If this is + undefined, skia will use a built-in value. +*/ +// #define SK_DEFAULT_FONT_CACHE_COUNT_LIMIT 2048 + +/* To specify the default size of the image cache, undefine this and set it to + the desired value (in bytes). SkGraphics.h as a runtime API to set this + value as well. If this is undefined, a built-in value will be used. +*/ +//#define SK_DEFAULT_IMAGE_CACHE_LIMIT (1024 * 1024) + +/* Define this to set the upper limit for text to support LCD. Values that + are very large increase the cost in the font cache and draw slower, without + improving readability. If this is undefined, Skia will use its default + value (e.g. 48) +*/ +//#define SK_MAX_SIZE_FOR_LCDTEXT 48 + +/* Change the kN32_SkColorType ordering to BGRA to work in X windows. +*/ +//#define SK_R32_SHIFT 16 + +/* Determines whether to build code that supports the Ganesh GPU backend. Some classes + that are not GPU-specific, such as SkShader subclasses, have optional code + that is used allows them to interact with this GPU backend. If you'd like to + include this code, include -DSK_GANESH in your cflags or uncomment below. + Defaults to not set (No Ganesh GPU backend). + This define affects the ABI of Skia, so make sure it matches the client which uses + the compiled version of Skia. +*/ +//#define SK_GANESH + +/* Skia makes use of histogram logging macros to trace the frequency of + events. By default, Skia provides no-op versions of these macros. + Skia consumers can provide their own definitions of these macros to + integrate with their histogram collection backend. +*/ +//#define SK_HISTOGRAM_BOOLEAN(name, sample) +//#define SK_HISTOGRAM_ENUMERATION(name, sample, enum_size) +//#define SK_HISTOGRAM_EXACT_LINEAR(name, sample, value_max) +//#define SK_HISTOGRAM_MEMORY_KB(name, sample) + +// To use smaller but slower mipmap builder +//#define SK_USE_DRAWING_MIPMAP_DOWNSAMPLER + +/* Skia tries to make use of some non-standard C++ language extensions. + By default, Skia provides msvc and clang/gcc versions of these macros. + Skia consumers can provide their own definitions of these macros to + integrate with their own compilers and build system. +*/ +//#define SK_ALWAYS_INLINE inline __attribute__((always_inline)) +//#define SK_NEVER_INLINE __attribute__((noinline)) +//#define SK_PRINTF_LIKE(A, B) __attribute__((format(printf, (A), (B)))) +//#define SK_NO_SANITIZE(A) __attribute__((no_sanitize(A))) +//#define SK_TRIVIAL_ABI [[clang::trivial_abi]] + +/* + * If compiling Skia as a DLL, public APIs should be exported. Skia will set + * SK_API to something sensible for Clang and MSVC, but if clients need to + * customize it for their build system or compiler, they may. + * If a client needs to use SK_API (e.g. overriding SK_ABORT), then they + * *must* define their own, the default will not be defined prior to loading + * this file. + */ +//#define SK_API __declspec(dllexport) + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkAlphaType.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkAlphaType.h new file mode 100644 index 00000000000..0c99906dfd5 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkAlphaType.h @@ -0,0 +1,45 @@ +/* + * Copyright 2022 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkAlphaType_DEFINED +#define SkAlphaType_DEFINED + +/** \enum SkAlphaType + Describes how to interpret the alpha component of a pixel. A pixel may + be opaque, or alpha, describing multiple levels of transparency. + + In simple blending, alpha weights the draw color and the destination + color to create a new color. If alpha describes a weight from zero to one: + + new color = draw color * alpha + destination color * (1 - alpha) + + In practice alpha is encoded in two or more bits, where 1.0 equals all bits set. + + RGB may have alpha included in each component value; the stored + value is the original RGB multiplied by alpha. Premultiplied color + components improve performance. +*/ +enum SkAlphaType : int { + kUnknown_SkAlphaType, //!< uninitialized + kOpaque_SkAlphaType, //!< pixel is opaque + kPremul_SkAlphaType, //!< pixel components are premultiplied by alpha + kUnpremul_SkAlphaType, //!< pixel components are independent of alpha + kLastEnum_SkAlphaType = kUnpremul_SkAlphaType, //!< last valid value +}; + +/** Returns true if SkAlphaType equals kOpaque_SkAlphaType. + + kOpaque_SkAlphaType is a hint that the SkColorType is opaque, or that all + alpha values are set to their 1.0 equivalent. If SkAlphaType is + kOpaque_SkAlphaType, and SkColorType is not opaque, then the result of + drawing any pixel with a alpha value less than 1.0 is undefined. +*/ +static inline bool SkAlphaTypeIsOpaque(SkAlphaType at) { + return kOpaque_SkAlphaType == at; +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkAnnotation.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkAnnotation.h new file mode 100644 index 00000000000..2006f309e94 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkAnnotation.h @@ -0,0 +1,52 @@ +/* + * Copyright 2012 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkAnnotation_DEFINED +#define SkAnnotation_DEFINED + +#include "include/core/SkTypes.h" + +class SkData; +struct SkPoint; +struct SkRect; +class SkCanvas; + +/** + * Annotate the canvas by associating the specified URL with the + * specified rectangle (in local coordinates, just like drawRect). + * + * The URL is expected to be escaped and be valid 7-bit ASCII. + * + * If the backend of this canvas does not support annotations, this call is + * safely ignored. + * + * The caller is responsible for managing its ownership of the SkData. + */ +SK_API void SkAnnotateRectWithURL(SkCanvas*, const SkRect&, SkData*); + +/** + * Annotate the canvas by associating a name with the specified point. + * + * If the backend of this canvas does not support annotations, this call is + * safely ignored. + * + * The caller is responsible for managing its ownership of the SkData. + */ +SK_API void SkAnnotateNamedDestination(SkCanvas*, const SkPoint&, SkData*); + +/** + * Annotate the canvas by making the specified rectangle link to a named + * destination. + * + * If the backend of this canvas does not support annotations, this call is + * safely ignored. + * + * The caller is responsible for managing its ownership of the SkData. + */ +SK_API void SkAnnotateLinkToDestination(SkCanvas*, const SkRect&, SkData*); + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkArc.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkArc.h new file mode 100644 index 00000000000..9e530ebd2b9 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkArc.h @@ -0,0 +1,69 @@ +/* + * Copyright 2024 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkArc_DEFINED +#define SkArc_DEFINED + +#include "include/core/SkRect.h" +#include "include/core/SkScalar.h" + +// Represents an arc along an oval boundary, or a closed wedge of the oval. +struct SkArc { + enum class Type : bool { + kArc, // An arc along the perimeter of the oval + kWedge // A closed wedge that includes the oval's center + }; + + SkArc() = default; + SkArc(const SkArc& arc) = default; + SkArc& operator=(const SkArc& arc) = default; + + const SkRect& oval() const { return fOval; } + SkScalar startAngle() const { return fStartAngle; } + SkScalar sweepAngle() const { return fSweepAngle; } + bool isWedge() const { return fType == Type::kWedge; } + + friend bool operator==(const SkArc& a, const SkArc& b) { + return a.fOval == b.fOval && a.fStartAngle == b.fStartAngle && + a.fSweepAngle == b.fSweepAngle && a.fType == b.fType; + } + + friend bool operator!=(const SkArc& a, const SkArc& b) { return !(a == b); } + + // Preferred factory that explicitly states which type of arc + static SkArc Make(const SkRect& oval, + SkScalar startAngleDegrees, + SkScalar sweepAngleDegrees, + Type type) { + return SkArc(oval, startAngleDegrees, sweepAngleDegrees, type); + } + + // Deprecated factory to assist with legacy code based on `useCenter` + static SkArc Make(const SkRect& oval, + SkScalar startAngleDegrees, + SkScalar sweepAngleDegrees, + bool useCenter) { + return SkArc( + oval, startAngleDegrees, sweepAngleDegrees, useCenter ? Type::kWedge : Type::kArc); + } + + // Bounds of oval containing the arc. + SkRect fOval = SkRect::MakeEmpty(); + + // Angle in degrees where the arc begins. Zero means horizontally to the right. + SkScalar fStartAngle = 0; + // Sweep angle in degrees; positive is clockwise. + SkScalar fSweepAngle = 0; + + Type fType = Type::kArc; + +private: + SkArc(const SkRect& oval, SkScalar startAngle, SkScalar sweepAngle, Type type) + : fOval(oval), fStartAngle(startAngle), fSweepAngle(sweepAngle), fType(type) {} +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBBHFactory.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBBHFactory.h new file mode 100644 index 00000000000..5d9f9009ac3 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBBHFactory.h @@ -0,0 +1,67 @@ +/* + * Copyright 2014 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkBBHFactory_DEFINED +#define SkBBHFactory_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" + +// TODO(kjlubick) fix client users and then make this a forward declare +#include "include/core/SkRect.h" // IWYU pragma: keep + +#include +#include + +class SkBBoxHierarchy : public SkRefCnt { +public: + struct Metadata { + bool isDraw; // The corresponding SkRect bounds a draw command, not a pure state change. + }; + + /** + * Insert N bounding boxes into the hierarchy. + */ + virtual void insert(const SkRect[], int N) = 0; + virtual void insert(const SkRect[], const Metadata[], int N); + + /** + * Populate results with the indices of bounding boxes intersecting that query. + */ + virtual void search(const SkRect& query, std::vector* results) const = 0; + + /** + * Return approximate size in memory of *this. + */ + virtual size_t bytesUsed() const = 0; + +protected: + SkBBoxHierarchy() = default; + SkBBoxHierarchy(const SkBBoxHierarchy&) = delete; + SkBBoxHierarchy& operator=(const SkBBoxHierarchy&) = delete; +}; + +class SK_API SkBBHFactory { +public: + /** + * Allocate a new SkBBoxHierarchy. Return NULL on failure. + */ + virtual sk_sp operator()() const = 0; + virtual ~SkBBHFactory() {} + +protected: + SkBBHFactory() = default; + SkBBHFactory(const SkBBHFactory&) = delete; + SkBBHFactory& operator=(const SkBBHFactory&) = delete; +}; + +class SK_API SkRTreeFactory : public SkBBHFactory { +public: + sk_sp operator()() const override; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBitmap.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBitmap.h new file mode 100644 index 00000000000..1875c5f37ed --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBitmap.h @@ -0,0 +1,1275 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkBitmap_DEFINED +#define SkBitmap_DEFINED + +#include "include/core/SkAlphaType.h" +#include "include/core/SkColor.h" +#include "include/core/SkImageInfo.h" +#include "include/core/SkPixmap.h" +#include "include/core/SkPoint.h" +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkSamplingOptions.h" +#include "include/core/SkSize.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkCPUTypes.h" +#include "include/private/base/SkDebug.h" + +#include +#include + +class SkColorSpace; +class SkImage; +class SkMatrix; +class SkMipmap; +class SkPaint; +class SkPixelRef; +class SkShader; +enum SkColorType : int; +enum class SkTileMode; +struct SkMaskBuilder; + +/** \class SkBitmap + SkBitmap describes a two-dimensional raster pixel array. SkBitmap is built on + SkImageInfo, containing integer width and height, SkColorType and SkAlphaType + describing the pixel format, and SkColorSpace describing the range of colors. + SkBitmap points to SkPixelRef, which describes the physical array of pixels. + SkImageInfo bounds may be located anywhere fully inside SkPixelRef bounds. + + SkBitmap can be drawn using SkCanvas. SkBitmap can be a drawing destination for SkCanvas + draw member functions. SkBitmap flexibility as a pixel container limits some + optimizations available to the target platform. + + If pixel array is primarily read-only, use SkImage for better performance. + If pixel array is primarily written to, use SkSurface for better performance. + + Declaring SkBitmap const prevents altering SkImageInfo: the SkBitmap height, width, + and so on cannot change. It does not affect SkPixelRef: a caller may write its + pixels. Declaring SkBitmap const affects SkBitmap configuration, not its contents. + + SkBitmap is not thread safe. Each thread must have its own copy of SkBitmap fields, + although threads may share the underlying pixel array. +*/ +class SK_API SkBitmap { +public: + class SK_API Allocator; + + /** Creates an empty SkBitmap without pixels, with kUnknown_SkColorType, + kUnknown_SkAlphaType, and with a width and height of zero. SkPixelRef origin is + set to (0, 0). + + Use setInfo() to associate SkColorType, SkAlphaType, width, and height + after SkBitmap has been created. + + @return empty SkBitmap + + example: https://fiddle.skia.org/c/@Bitmap_empty_constructor + */ + SkBitmap(); + + /** Copies settings from src to returned SkBitmap. Shares pixels if src has pixels + allocated, so both bitmaps reference the same pixels. + + @param src SkBitmap to copy SkImageInfo, and share SkPixelRef + @return copy of src + + example: https://fiddle.skia.org/c/@Bitmap_copy_const_SkBitmap + */ + SkBitmap(const SkBitmap& src); + + /** Copies settings from src to returned SkBitmap. Moves ownership of src pixels to + SkBitmap. + + @param src SkBitmap to copy SkImageInfo, and reassign SkPixelRef + @return copy of src + + example: https://fiddle.skia.org/c/@Bitmap_move_SkBitmap + */ + SkBitmap(SkBitmap&& src); + + /** Decrements SkPixelRef reference count, if SkPixelRef is not nullptr. + */ + ~SkBitmap(); + + /** Copies settings from src to returned SkBitmap. Shares pixels if src has pixels + allocated, so both bitmaps reference the same pixels. + + @param src SkBitmap to copy SkImageInfo, and share SkPixelRef + @return copy of src + + example: https://fiddle.skia.org/c/@Bitmap_copy_operator + */ + SkBitmap& operator=(const SkBitmap& src); + + /** Copies settings from src to returned SkBitmap. Moves ownership of src pixels to + SkBitmap. + + @param src SkBitmap to copy SkImageInfo, and reassign SkPixelRef + @return copy of src + + example: https://fiddle.skia.org/c/@Bitmap_move_operator + */ + SkBitmap& operator=(SkBitmap&& src); + + /** Swaps the fields of the two bitmaps. + + @param other SkBitmap exchanged with original + + example: https://fiddle.skia.org/c/@Bitmap_swap + */ + void swap(SkBitmap& other); + + /** Returns a constant reference to the SkPixmap holding the SkBitmap pixel + address, row bytes, and SkImageInfo. + + @return reference to SkPixmap describing this SkBitmap + */ + const SkPixmap& pixmap() const { return fPixmap; } + + /** Returns width, height, SkAlphaType, SkColorType, and SkColorSpace. + + @return reference to SkImageInfo + */ + const SkImageInfo& info() const { return fPixmap.info(); } + + /** Returns pixel count in each row. Should be equal or less than + rowBytes() / info().bytesPerPixel(). + + May be less than pixelRef().width(). Will not exceed pixelRef().width() less + pixelRefOrigin().fX. + + @return pixel width in SkImageInfo + */ + int width() const { return fPixmap.width(); } + + /** Returns pixel row count. + + Maybe be less than pixelRef().height(). Will not exceed pixelRef().height() less + pixelRefOrigin().fY. + + @return pixel height in SkImageInfo + */ + int height() const { return fPixmap.height(); } + + SkColorType colorType() const { return fPixmap.colorType(); } + + SkAlphaType alphaType() const { return fPixmap.alphaType(); } + + /** Returns SkColorSpace, the range of colors, associated with SkImageInfo. The + reference count of SkColorSpace is unchanged. The returned SkColorSpace is + immutable. + + @return SkColorSpace in SkImageInfo, or nullptr + */ + SkColorSpace* colorSpace() const; + + /** Returns smart pointer to SkColorSpace, the range of colors, associated with + SkImageInfo. The smart pointer tracks the number of objects sharing this + SkColorSpace reference so the memory is released when the owners destruct. + + The returned SkColorSpace is immutable. + + @return SkColorSpace in SkImageInfo wrapped in a smart pointer + */ + sk_sp refColorSpace() const; + + /** Returns number of bytes per pixel required by SkColorType. + Returns zero if colorType( is kUnknown_SkColorType. + + @return bytes in pixel + */ + int bytesPerPixel() const { return fPixmap.info().bytesPerPixel(); } + + /** Returns number of pixels that fit on row. Should be greater than or equal to + width(). + + @return maximum pixels per row + */ + int rowBytesAsPixels() const { return fPixmap.rowBytesAsPixels(); } + + /** Returns bit shift converting row bytes to row pixels. + Returns zero for kUnknown_SkColorType. + + @return one of: 0, 1, 2, 3; left shift to convert pixels to bytes + */ + int shiftPerPixel() const { return fPixmap.shiftPerPixel(); } + + /** Returns true if either width() or height() are zero. + + Does not check if SkPixelRef is nullptr; call drawsNothing() to check width(), + height(), and SkPixelRef. + + @return true if dimensions do not enclose area + */ + bool empty() const { return fPixmap.info().isEmpty(); } + + /** Returns true if SkPixelRef is nullptr. + + Does not check if width() or height() are zero; call drawsNothing() to check + width(), height(), and SkPixelRef. + + @return true if no SkPixelRef is associated + */ + bool isNull() const { return nullptr == fPixelRef; } + + /** Returns true if width() or height() are zero, or if SkPixelRef is nullptr. + If true, SkBitmap has no effect when drawn or drawn into. + + @return true if drawing has no effect + */ + bool drawsNothing() const { + return this->empty() || this->isNull(); + } + + /** Returns row bytes, the interval from one pixel row to the next. Row bytes + is at least as large as: width() * info().bytesPerPixel(). + + Returns zero if colorType() is kUnknown_SkColorType, or if row bytes supplied to + setInfo() is not large enough to hold a row of pixels. + + @return byte length of pixel row + */ + size_t rowBytes() const { return fPixmap.rowBytes(); } + + /** Sets SkAlphaType, if alphaType is compatible with SkColorType. + Returns true unless alphaType is kUnknown_SkAlphaType and current SkAlphaType + is not kUnknown_SkAlphaType. + + Returns true if SkColorType is kUnknown_SkColorType. alphaType is ignored, and + SkAlphaType remains kUnknown_SkAlphaType. + + Returns true if SkColorType is kRGB_565_SkColorType or kGray_8_SkColorType. + alphaType is ignored, and SkAlphaType remains kOpaque_SkAlphaType. + + If SkColorType is kARGB_4444_SkColorType, kRGBA_8888_SkColorType, + kBGRA_8888_SkColorType, or kRGBA_F16_SkColorType: returns true unless + alphaType is kUnknown_SkAlphaType and SkAlphaType is not kUnknown_SkAlphaType. + If SkAlphaType is kUnknown_SkAlphaType, alphaType is ignored. + + If SkColorType is kAlpha_8_SkColorType, returns true unless + alphaType is kUnknown_SkAlphaType and SkAlphaType is not kUnknown_SkAlphaType. + If SkAlphaType is kUnknown_SkAlphaType, alphaType is ignored. If alphaType is + kUnpremul_SkAlphaType, it is treated as kPremul_SkAlphaType. + + This changes SkAlphaType in SkPixelRef; all bitmaps sharing SkPixelRef + are affected. + + @return true if SkAlphaType is set + + example: https://fiddle.skia.org/c/@Bitmap_setAlphaType + */ + bool setAlphaType(SkAlphaType alphaType); + + /** Sets the SkColorSpace associated with this SkBitmap. + + The raw pixel data is not altered by this call; no conversion is + performed. + + This changes SkColorSpace in SkPixelRef; all bitmaps sharing SkPixelRef + are affected. + */ + void setColorSpace(sk_sp colorSpace); + + /** Returns pixel address, the base address corresponding to the pixel origin. + + @return pixel address + */ + void* getPixels() const { return fPixmap.writable_addr(); } + + /** Returns minimum memory required for pixel storage. + Does not include unused memory on last row when rowBytesAsPixels() exceeds width(). + Returns SIZE_MAX if result does not fit in size_t. + Returns zero if height() or width() is 0. + Returns height() times rowBytes() if colorType() is kUnknown_SkColorType. + + @return size in bytes of image buffer + */ + size_t computeByteSize() const { return fPixmap.computeByteSize(); } + + /** Returns true if pixels can not change. + + Most immutable SkBitmap checks trigger an assert only on debug builds. + + @return true if pixels are immutable + + example: https://fiddle.skia.org/c/@Bitmap_isImmutable + */ + bool isImmutable() const; + + /** Sets internal flag to mark SkBitmap as immutable. Once set, pixels can not change. + Any other bitmap sharing the same SkPixelRef are also marked as immutable. + Once SkPixelRef is marked immutable, the setting cannot be cleared. + + Writing to immutable SkBitmap pixels triggers an assert on debug builds. + + example: https://fiddle.skia.org/c/@Bitmap_setImmutable + */ + void setImmutable(); + + /** Returns true if SkAlphaType is set to hint that all pixels are opaque; their + alpha value is implicitly or explicitly 1.0. If true, and all pixels are + not opaque, Skia may draw incorrectly. + + Does not check if SkColorType allows alpha, or if any pixel value has + transparency. + + @return true if SkImageInfo SkAlphaType is kOpaque_SkAlphaType + */ + bool isOpaque() const { + return SkAlphaTypeIsOpaque(this->alphaType()); + } + + /** Resets to its initial state; all fields are set to zero, as if SkBitmap had + been initialized by SkBitmap(). + + Sets width, height, row bytes to zero; pixel address to nullptr; SkColorType to + kUnknown_SkColorType; and SkAlphaType to kUnknown_SkAlphaType. + + If SkPixelRef is allocated, its reference count is decreased by one, releasing + its memory if SkBitmap is the sole owner. + + example: https://fiddle.skia.org/c/@Bitmap_reset + */ + void reset(); + + /** Returns true if all pixels are opaque. SkColorType determines how pixels + are encoded, and whether pixel describes alpha. Returns true for SkColorType + without alpha in each pixel; for other SkColorType, returns true if all + pixels have alpha values equivalent to 1.0 or greater. + + For SkColorType kRGB_565_SkColorType or kGray_8_SkColorType: always + returns true. For SkColorType kAlpha_8_SkColorType, kBGRA_8888_SkColorType, + kRGBA_8888_SkColorType: returns true if all pixel alpha values are 255. + For SkColorType kARGB_4444_SkColorType: returns true if all pixel alpha values are 15. + For kRGBA_F16_SkColorType: returns true if all pixel alpha values are 1.0 or + greater. + + Returns false for kUnknown_SkColorType. + + @param bm SkBitmap to check + @return true if all pixels have opaque values or SkColorType is opaque + */ + static bool ComputeIsOpaque(const SkBitmap& bm) { + return bm.pixmap().computeIsOpaque(); + } + + /** Returns SkRect { 0, 0, width(), height() }. + + @param bounds container for floating point rectangle + + example: https://fiddle.skia.org/c/@Bitmap_getBounds + */ + void getBounds(SkRect* bounds) const; + + /** Returns SkIRect { 0, 0, width(), height() }. + + @param bounds container for integral rectangle + + example: https://fiddle.skia.org/c/@Bitmap_getBounds_2 + */ + void getBounds(SkIRect* bounds) const; + + /** Returns SkIRect { 0, 0, width(), height() }. + + @return integral rectangle from origin to width() and height() + */ + SkIRect bounds() const { return fPixmap.info().bounds(); } + + /** Returns SkISize { width(), height() }. + + @return integral size of width() and height() + */ + SkISize dimensions() const { return fPixmap.info().dimensions(); } + + /** Returns the bounds of this bitmap, offset by its SkPixelRef origin. + + @return bounds within SkPixelRef bounds + */ + SkIRect getSubset() const { + SkIPoint origin = this->pixelRefOrigin(); + return SkIRect::MakeXYWH(origin.x(), origin.y(), this->width(), this->height()); + } + + /** Sets width, height, SkAlphaType, SkColorType, SkColorSpace, and optional + rowBytes. Frees pixels, and returns true if successful. + + imageInfo.alphaType() may be altered to a value permitted by imageInfo.colorSpace(). + If imageInfo.colorType() is kUnknown_SkColorType, imageInfo.alphaType() is + set to kUnknown_SkAlphaType. + If imageInfo.colorType() is kAlpha_8_SkColorType and imageInfo.alphaType() is + kUnpremul_SkAlphaType, imageInfo.alphaType() is replaced by kPremul_SkAlphaType. + If imageInfo.colorType() is kRGB_565_SkColorType or kGray_8_SkColorType, + imageInfo.alphaType() is set to kOpaque_SkAlphaType. + If imageInfo.colorType() is kARGB_4444_SkColorType, kRGBA_8888_SkColorType, + kBGRA_8888_SkColorType, or kRGBA_F16_SkColorType: imageInfo.alphaType() remains + unchanged. + + rowBytes must equal or exceed imageInfo.minRowBytes(). If imageInfo.colorSpace() is + kUnknown_SkColorType, rowBytes is ignored and treated as zero; for all other + SkColorSpace values, rowBytes of zero is treated as imageInfo.minRowBytes(). + + Calls reset() and returns false if: + - rowBytes exceeds 31 bits + - imageInfo.width() is negative + - imageInfo.height() is negative + - rowBytes is positive and less than imageInfo.width() times imageInfo.bytesPerPixel() + + @param imageInfo contains width, height, SkAlphaType, SkColorType, SkColorSpace + @param rowBytes imageInfo.minRowBytes() or larger; or zero + @return true if SkImageInfo set successfully + + example: https://fiddle.skia.org/c/@Bitmap_setInfo + */ + bool setInfo(const SkImageInfo& imageInfo, size_t rowBytes = 0); + + /** \enum SkBitmap::AllocFlags + AllocFlags is obsolete. We always zero pixel memory when allocated. + */ + enum AllocFlags { + kZeroPixels_AllocFlag = 1 << 0, //!< zero pixel memory. No effect. This is the default. + }; + + /** Sets SkImageInfo to info following the rules in setInfo() and allocates pixel + memory. Memory is zeroed. + + Returns false and calls reset() if SkImageInfo could not be set, or memory could + not be allocated, or memory could not optionally be zeroed. + + On most platforms, allocating pixel memory may succeed even though there is + not sufficient memory to hold pixels; allocation does not take place + until the pixels are written to. The actual behavior depends on the platform + implementation of calloc(). + + @param info contains width, height, SkAlphaType, SkColorType, SkColorSpace + @param flags kZeroPixels_AllocFlag, or zero + @return true if pixels allocation is successful + */ + [[nodiscard]] bool tryAllocPixelsFlags(const SkImageInfo& info, uint32_t flags); + + /** Sets SkImageInfo to info following the rules in setInfo() and allocates pixel + memory. Memory is zeroed. + + Aborts execution if SkImageInfo could not be set, or memory could + not be allocated, or memory could not optionally + be zeroed. Abort steps may be provided by the user at compile time by defining + SK_ABORT. + + On most platforms, allocating pixel memory may succeed even though there is + not sufficient memory to hold pixels; allocation does not take place + until the pixels are written to. The actual behavior depends on the platform + implementation of calloc(). + + @param info contains width, height, SkAlphaType, SkColorType, SkColorSpace + @param flags kZeroPixels_AllocFlag, or zero + + example: https://fiddle.skia.org/c/@Bitmap_allocPixelsFlags + */ + void allocPixelsFlags(const SkImageInfo& info, uint32_t flags); + + /** Sets SkImageInfo to info following the rules in setInfo() and allocates pixel + memory. rowBytes must equal or exceed info.width() times info.bytesPerPixel(), + or equal zero. Pass in zero for rowBytes to compute the minimum valid value. + + Returns false and calls reset() if SkImageInfo could not be set, or memory could + not be allocated. + + On most platforms, allocating pixel memory may succeed even though there is + not sufficient memory to hold pixels; allocation does not take place + until the pixels are written to. The actual behavior depends on the platform + implementation of malloc(). + + @param info contains width, height, SkAlphaType, SkColorType, SkColorSpace + @param rowBytes size of pixel row or larger; may be zero + @return true if pixel storage is allocated + */ + [[nodiscard]] bool tryAllocPixels(const SkImageInfo& info, size_t rowBytes); + + /** Sets SkImageInfo to info following the rules in setInfo() and allocates pixel + memory. rowBytes must equal or exceed info.width() times info.bytesPerPixel(), + or equal zero. Pass in zero for rowBytes to compute the minimum valid value. + + Aborts execution if SkImageInfo could not be set, or memory could + not be allocated. Abort steps may be provided by + the user at compile time by defining SK_ABORT. + + On most platforms, allocating pixel memory may succeed even though there is + not sufficient memory to hold pixels; allocation does not take place + until the pixels are written to. The actual behavior depends on the platform + implementation of malloc(). + + @param info contains width, height, SkAlphaType, SkColorType, SkColorSpace + @param rowBytes size of pixel row or larger; may be zero + + example: https://fiddle.skia.org/c/@Bitmap_allocPixels + */ + void allocPixels(const SkImageInfo& info, size_t rowBytes); + + /** Sets SkImageInfo to info following the rules in setInfo() and allocates pixel + memory. + + Returns false and calls reset() if SkImageInfo could not be set, or memory could + not be allocated. + + On most platforms, allocating pixel memory may succeed even though there is + not sufficient memory to hold pixels; allocation does not take place + until the pixels are written to. The actual behavior depends on the platform + implementation of malloc(). + + @param info contains width, height, SkAlphaType, SkColorType, SkColorSpace + @return true if pixel storage is allocated + */ + [[nodiscard]] bool tryAllocPixels(const SkImageInfo& info) { + return this->tryAllocPixels(info, info.minRowBytes()); + } + + /** Sets SkImageInfo to info following the rules in setInfo() and allocates pixel + memory. + + Aborts execution if SkImageInfo could not be set, or memory could + not be allocated. Abort steps may be provided by + the user at compile time by defining SK_ABORT. + + On most platforms, allocating pixel memory may succeed even though there is + not sufficient memory to hold pixels; allocation does not take place + until the pixels are written to. The actual behavior depends on the platform + implementation of malloc(). + + @param info contains width, height, SkAlphaType, SkColorType, SkColorSpace + + example: https://fiddle.skia.org/c/@Bitmap_allocPixels_2 + */ + void allocPixels(const SkImageInfo& info); + + /** Sets SkImageInfo to width, height, and native color type; and allocates + pixel memory. If isOpaque is true, sets SkImageInfo to kOpaque_SkAlphaType; + otherwise, sets to kPremul_SkAlphaType. + + Calls reset() and returns false if width exceeds 29 bits or is negative, + or height is negative. + + Returns false if allocation fails. + + Use to create SkBitmap that matches SkPMColor, the native pixel arrangement on + the platform. SkBitmap drawn to output device skips converting its pixel format. + + @param width pixel column count; must be zero or greater + @param height pixel row count; must be zero or greater + @param isOpaque true if pixels do not have transparency + @return true if pixel storage is allocated + */ + [[nodiscard]] bool tryAllocN32Pixels(int width, int height, bool isOpaque = false); + + /** Sets SkImageInfo to width, height, and the native color type; and allocates + pixel memory. If isOpaque is true, sets SkImageInfo to kOpaque_SkAlphaType; + otherwise, sets to kPremul_SkAlphaType. + + Aborts if width exceeds 29 bits or is negative, or height is negative, or + allocation fails. Abort steps may be provided by the user at compile time by + defining SK_ABORT. + + Use to create SkBitmap that matches SkPMColor, the native pixel arrangement on + the platform. SkBitmap drawn to output device skips converting its pixel format. + + @param width pixel column count; must be zero or greater + @param height pixel row count; must be zero or greater + @param isOpaque true if pixels do not have transparency + + example: https://fiddle.skia.org/c/@Bitmap_allocN32Pixels + */ + void allocN32Pixels(int width, int height, bool isOpaque = false); + + /** Sets SkImageInfo to info following the rules in setInfo(), and creates SkPixelRef + containing pixels and rowBytes. releaseProc, if not nullptr, is called + immediately on failure or when pixels are no longer referenced. context may be + nullptr. + + If SkImageInfo could not be set, or rowBytes is less than info.minRowBytes(): + calls releaseProc if present, calls reset(), and returns false. + + Otherwise, if pixels equals nullptr: sets SkImageInfo, calls releaseProc if + present, returns true. + + If SkImageInfo is set, pixels is not nullptr, and releaseProc is not nullptr: + when pixels are no longer referenced, calls releaseProc with pixels and context + as parameters. + + @param info contains width, height, SkAlphaType, SkColorType, SkColorSpace + @param pixels address or pixel storage; may be nullptr + @param rowBytes size of pixel row or larger + @param releaseProc function called when pixels can be deleted; may be nullptr + @param context caller state passed to releaseProc; may be nullptr + @return true if SkImageInfo is set to info + */ + bool installPixels(const SkImageInfo& info, void* pixels, size_t rowBytes, + void (*releaseProc)(void* addr, void* context), void* context); + + /** Sets SkImageInfo to info following the rules in setInfo(), and creates SkPixelRef + containing pixels and rowBytes. + + If SkImageInfo could not be set, or rowBytes is less than info.minRowBytes(): + calls reset(), and returns false. + + Otherwise, if pixels equals nullptr: sets SkImageInfo, returns true. + + Caller must ensure that pixels are valid for the lifetime of SkBitmap and SkPixelRef. + + @param info contains width, height, SkAlphaType, SkColorType, SkColorSpace + @param pixels address or pixel storage; may be nullptr + @param rowBytes size of pixel row or larger + @return true if SkImageInfo is set to info + */ + bool installPixels(const SkImageInfo& info, void* pixels, size_t rowBytes) { + return this->installPixels(info, pixels, rowBytes, nullptr, nullptr); + } + + /** Sets SkImageInfo to pixmap.info() following the rules in setInfo(), and creates + SkPixelRef containing pixmap.addr() and pixmap.rowBytes(). + + If SkImageInfo could not be set, or pixmap.rowBytes() is less than + SkImageInfo::minRowBytes(): calls reset(), and returns false. + + Otherwise, if pixmap.addr() equals nullptr: sets SkImageInfo, returns true. + + Caller must ensure that pixmap is valid for the lifetime of SkBitmap and SkPixelRef. + + @param pixmap SkImageInfo, pixel address, and rowBytes() + @return true if SkImageInfo was set to pixmap.info() + + example: https://fiddle.skia.org/c/@Bitmap_installPixels_3 + */ + bool installPixels(const SkPixmap& pixmap); + + /** Deprecated. + */ + bool installMaskPixels(SkMaskBuilder& mask); + + /** Replaces SkPixelRef with pixels, preserving SkImageInfo and rowBytes(). + Sets SkPixelRef origin to (0, 0). + + If pixels is nullptr, or if info().colorType() equals kUnknown_SkColorType; + release reference to SkPixelRef, and set SkPixelRef to nullptr. + + Caller is responsible for handling ownership pixel memory for the lifetime + of SkBitmap and SkPixelRef. + + @param pixels address of pixel storage, managed by caller + + example: https://fiddle.skia.org/c/@Bitmap_setPixels + */ + void setPixels(void* pixels); + + /** Allocates pixel memory with HeapAllocator, and replaces existing SkPixelRef. + The allocation size is determined by SkImageInfo width, height, and SkColorType. + + Returns false if info().colorType() is kUnknown_SkColorType, or allocation fails. + + @return true if the allocation succeeds + */ + [[nodiscard]] bool tryAllocPixels() { + return this->tryAllocPixels((Allocator*)nullptr); + } + + /** Allocates pixel memory with HeapAllocator, and replaces existing SkPixelRef. + The allocation size is determined by SkImageInfo width, height, and SkColorType. + + Aborts if info().colorType() is kUnknown_SkColorType, or allocation fails. + Abort steps may be provided by the user at compile + time by defining SK_ABORT. + + example: https://fiddle.skia.org/c/@Bitmap_allocPixels_3 + */ + void allocPixels(); + + /** Allocates pixel memory with allocator, and replaces existing SkPixelRef. + The allocation size is determined by SkImageInfo width, height, and SkColorType. + If allocator is nullptr, use HeapAllocator instead. + + Returns false if Allocator::allocPixelRef return false. + + @param allocator instance of SkBitmap::Allocator instantiation + @return true if custom allocator reports success + */ + [[nodiscard]] bool tryAllocPixels(Allocator* allocator); + + /** Allocates pixel memory with allocator, and replaces existing SkPixelRef. + The allocation size is determined by SkImageInfo width, height, and SkColorType. + If allocator is nullptr, use HeapAllocator instead. + + Aborts if Allocator::allocPixelRef return false. Abort steps may be provided by + the user at compile time by defining SK_ABORT. + + @param allocator instance of SkBitmap::Allocator instantiation + + example: https://fiddle.skia.org/c/@Bitmap_allocPixels_4 + */ + void allocPixels(Allocator* allocator); + + /** Returns SkPixelRef, which contains: pixel base address; its dimensions; and + rowBytes(), the interval from one row to the next. Does not change SkPixelRef + reference count. SkPixelRef may be shared by multiple bitmaps. + If SkPixelRef has not been set, returns nullptr. + + @return SkPixelRef, or nullptr + */ + SkPixelRef* pixelRef() const { return fPixelRef.get(); } + + /** Returns origin of pixels within SkPixelRef. SkBitmap bounds is always contained + by SkPixelRef bounds, which may be the same size or larger. Multiple SkBitmap + can share the same SkPixelRef, where each SkBitmap has different bounds. + + The returned origin added to SkBitmap dimensions equals or is smaller than the + SkPixelRef dimensions. + + Returns (0, 0) if SkPixelRef is nullptr. + + @return pixel origin within SkPixelRef + + example: https://fiddle.skia.org/c/@Bitmap_pixelRefOrigin + */ + SkIPoint pixelRefOrigin() const; + + /** Replaces pixelRef and origin in SkBitmap. dx and dy specify the offset + within the SkPixelRef pixels for the top-left corner of the bitmap. + + Asserts in debug builds if dx or dy are out of range. Pins dx and dy + to legal range in release builds. + + The caller is responsible for ensuring that the pixels match the + SkColorType and SkAlphaType in SkImageInfo. + + @param pixelRef SkPixelRef describing pixel address and rowBytes() + @param dx column offset in SkPixelRef for bitmap origin + @param dy row offset in SkPixelRef for bitmap origin + + example: https://fiddle.skia.org/c/@Bitmap_setPixelRef + */ + void setPixelRef(sk_sp pixelRef, int dx, int dy); + + /** Returns true if SkBitmap is can be drawn. + + @return true if getPixels() is not nullptr + */ + bool readyToDraw() const { + return this->getPixels() != nullptr; + } + + /** Returns a unique value corresponding to the pixels in SkPixelRef. + Returns a different value after notifyPixelsChanged() has been called. + Returns zero if SkPixelRef is nullptr. + + Determines if pixels have changed since last examined. + + @return unique value for pixels in SkPixelRef + + example: https://fiddle.skia.org/c/@Bitmap_getGenerationID + */ + uint32_t getGenerationID() const; + + /** Marks that pixels in SkPixelRef have changed. Subsequent calls to + getGenerationID() return a different value. + + example: https://fiddle.skia.org/c/@Bitmap_notifyPixelsChanged + */ + void notifyPixelsChanged() const; + + /** Replaces pixel values with c, interpreted as being in the sRGB SkColorSpace. + All pixels contained by bounds() are affected. If the colorType() is + kGray_8_SkColorType or kRGB_565_SkColorType, then alpha is ignored; RGB is + treated as opaque. If colorType() is kAlpha_8_SkColorType, then RGB is ignored. + + @param c unpremultiplied color + + example: https://fiddle.skia.org/c/@Bitmap_eraseColor + */ + void eraseColor(SkColor4f) const; + + /** Replaces pixel values with c, interpreted as being in the sRGB SkColorSpace. + All pixels contained by bounds() are affected. If the colorType() is + kGray_8_SkColorType or kRGB_565_SkColorType, then alpha is ignored; RGB is + treated as opaque. If colorType() is kAlpha_8_SkColorType, then RGB is ignored. + + Input color is ultimately converted to an SkColor4f, so eraseColor(SkColor4f c) + will have higher color resolution. + + @param c unpremultiplied color. + + example: https://fiddle.skia.org/c/@Bitmap_eraseColor + */ + void eraseColor(SkColor c) const; + + /** Replaces pixel values with unpremultiplied color built from a, r, g, and b, + interpreted as being in the sRGB SkColorSpace. All pixels contained by + bounds() are affected. If the colorType() is kGray_8_SkColorType or + kRGB_565_SkColorType, then a is ignored; r, g, and b are treated as opaque. + If colorType() is kAlpha_8_SkColorType, then r, g, and b are ignored. + + @param a amount of alpha, from fully transparent (0) to fully opaque (255) + @param r amount of red, from no red (0) to full red (255) + @param g amount of green, from no green (0) to full green (255) + @param b amount of blue, from no blue (0) to full blue (255) + */ + void eraseARGB(U8CPU a, U8CPU r, U8CPU g, U8CPU b) const { + this->eraseColor(SkColorSetARGB(a, r, g, b)); + } + + /** Replaces pixel values inside area with c. interpreted as being in the sRGB + SkColorSpace. If area does not intersect bounds(), call has no effect. + + If the colorType() is kGray_8_SkColorType or kRGB_565_SkColorType, then alpha + is ignored; RGB is treated as opaque. If colorType() is kAlpha_8_SkColorType, + then RGB is ignored. + + @param c unpremultiplied color + @param area rectangle to fill + + example: https://fiddle.skia.org/c/@Bitmap_erase + */ + void erase(SkColor4f c, const SkIRect& area) const; + + /** Replaces pixel values inside area with c. interpreted as being in the sRGB + SkColorSpace. If area does not intersect bounds(), call has no effect. + + If the colorType() is kGray_8_SkColorType or kRGB_565_SkColorType, then alpha + is ignored; RGB is treated as opaque. If colorType() is kAlpha_8_SkColorType, + then RGB is ignored. + + Input color is ultimately converted to an SkColor4f, so erase(SkColor4f c) + will have higher color resolution. + + @param c unpremultiplied color + @param area rectangle to fill + + example: https://fiddle.skia.org/c/@Bitmap_erase + */ + void erase(SkColor c, const SkIRect& area) const; + + /** Deprecated. + */ + void eraseArea(const SkIRect& area, SkColor c) const { + this->erase(c, area); + } + + /** Returns pixel at (x, y) as unpremultiplied color. + Returns black with alpha if SkColorType is kAlpha_8_SkColorType. + + Input is not validated: out of bounds values of x or y trigger an assert() if + built with SK_DEBUG defined; and returns undefined values or may crash if + SK_RELEASE is defined. Fails if SkColorType is kUnknown_SkColorType or + pixel address is nullptr. + + SkColorSpace in SkImageInfo is ignored. Some color precision may be lost in the + conversion to unpremultiplied color; original pixel data may have additional + precision. + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return pixel converted to unpremultiplied color + */ + SkColor getColor(int x, int y) const { + return this->pixmap().getColor(x, y); + } + + /** Returns pixel at (x, y) as unpremultiplied float color. + Returns black with alpha if SkColorType is kAlpha_8_SkColorType. + + Input is not validated: out of bounds values of x or y trigger an assert() if + built with SK_DEBUG defined; and returns undefined values or may crash if + SK_RELEASE is defined. Fails if SkColorType is kUnknown_SkColorType or + pixel address is nullptr. + + SkColorSpace in SkImageInfo is ignored. Some color precision may be lost in the + conversion to unpremultiplied color. + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return pixel converted to unpremultiplied color + */ + SkColor4f getColor4f(int x, int y) const { return this->pixmap().getColor4f(x, y); } + + /** Look up the pixel at (x,y) and return its alpha component, normalized to [0..1]. + This is roughly equivalent to SkGetColorA(getColor()), but can be more efficent + (and more precise if the pixels store more than 8 bits per component). + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return alpha converted to normalized float + */ + float getAlphaf(int x, int y) const { + return this->pixmap().getAlphaf(x, y); + } + + /** Returns pixel address at (x, y). + + Input is not validated: out of bounds values of x or y, or kUnknown_SkColorType, + trigger an assert() if built with SK_DEBUG defined. Returns nullptr if + SkColorType is kUnknown_SkColorType, or SkPixelRef is nullptr. + + Performs a lookup of pixel size; for better performance, call + one of: getAddr8(), getAddr16(), or getAddr32(). + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return generic pointer to pixel + + example: https://fiddle.skia.org/c/@Bitmap_getAddr + */ + void* getAddr(int x, int y) const; + + /** Returns address at (x, y). + + Input is not validated. Triggers an assert() if built with SK_DEBUG defined and: + - SkPixelRef is nullptr + - bytesPerPixel() is not four + - x is negative, or not less than width() + - y is negative, or not less than height() + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return unsigned 32-bit pointer to pixel at (x, y) + */ + inline uint32_t* getAddr32(int x, int y) const; + + /** Returns address at (x, y). + + Input is not validated. Triggers an assert() if built with SK_DEBUG defined and: + - SkPixelRef is nullptr + - bytesPerPixel() is not two + - x is negative, or not less than width() + - y is negative, or not less than height() + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return unsigned 16-bit pointer to pixel at (x, y) + */ + inline uint16_t* getAddr16(int x, int y) const; + + /** Returns address at (x, y). + + Input is not validated. Triggers an assert() if built with SK_DEBUG defined and: + - SkPixelRef is nullptr + - bytesPerPixel() is not one + - x is negative, or not less than width() + - y is negative, or not less than height() + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return unsigned 8-bit pointer to pixel at (x, y) + */ + inline uint8_t* getAddr8(int x, int y) const; + + /** Shares SkPixelRef with dst. Pixels are not copied; SkBitmap and dst point + to the same pixels; dst bounds() are set to the intersection of subset + and the original bounds(). + + subset may be larger than bounds(). Any area outside of bounds() is ignored. + + Any contents of dst are discarded. + + Return false if: + - dst is nullptr + - SkPixelRef is nullptr + - subset does not intersect bounds() + + @param dst SkBitmap set to subset + @param subset rectangle of pixels to reference + @return true if dst is replaced by subset + + example: https://fiddle.skia.org/c/@Bitmap_extractSubset + */ + bool extractSubset(SkBitmap* dst, const SkIRect& subset) const; + + /** Copies a SkRect of pixels from SkBitmap to dstPixels. Copy starts at (srcX, srcY), + and does not exceed SkBitmap (width(), height()). + + dstInfo specifies width, height, SkColorType, SkAlphaType, and SkColorSpace of + destination. dstRowBytes specifics the gap from one destination row to the next. + Returns true if pixels are copied. Returns false if: + - dstInfo has no address + - dstRowBytes is less than dstInfo.minRowBytes() + - SkPixelRef is nullptr + + Pixels are copied only if pixel conversion is possible. If SkBitmap colorType() is + kGray_8_SkColorType, or kAlpha_8_SkColorType; dstInfo.colorType() must match. + If SkBitmap colorType() is kGray_8_SkColorType, dstInfo.colorSpace() must match. + If SkBitmap alphaType() is kOpaque_SkAlphaType, dstInfo.alphaType() must + match. If SkBitmap colorSpace() is nullptr, dstInfo.colorSpace() must match. Returns + false if pixel conversion is not possible. + + srcX and srcY may be negative to copy only top or left of source. Returns + false if width() or height() is zero or negative. + Returns false if abs(srcX) >= Bitmap width(), or if abs(srcY) >= Bitmap height(). + + @param dstInfo destination width, height, SkColorType, SkAlphaType, SkColorSpace + @param dstPixels destination pixel storage + @param dstRowBytes destination row length + @param srcX column index whose absolute value is less than width() + @param srcY row index whose absolute value is less than height() + @return true if pixels are copied to dstPixels + */ + bool readPixels(const SkImageInfo& dstInfo, void* dstPixels, size_t dstRowBytes, + int srcX, int srcY) const; + + /** Copies a SkRect of pixels from SkBitmap to dst. Copy starts at (srcX, srcY), and + does not exceed SkBitmap (width(), height()). + + dst specifies width, height, SkColorType, SkAlphaType, SkColorSpace, pixel storage, + and row bytes of destination. dst.rowBytes() specifics the gap from one destination + row to the next. Returns true if pixels are copied. Returns false if: + - dst pixel storage equals nullptr + - dst.rowBytes is less than SkImageInfo::minRowBytes() + - SkPixelRef is nullptr + + Pixels are copied only if pixel conversion is possible. If SkBitmap colorType() is + kGray_8_SkColorType, or kAlpha_8_SkColorType; dst SkColorType must match. + If SkBitmap colorType() is kGray_8_SkColorType, dst SkColorSpace must match. + If SkBitmap alphaType() is kOpaque_SkAlphaType, dst SkAlphaType must + match. If SkBitmap colorSpace() is nullptr, dst SkColorSpace must match. Returns + false if pixel conversion is not possible. + + srcX and srcY may be negative to copy only top or left of source. Returns + false if width() or height() is zero or negative. + Returns false if abs(srcX) >= Bitmap width(), or if abs(srcY) >= Bitmap height(). + + @param dst destination SkPixmap: SkImageInfo, pixels, row bytes + @param srcX column index whose absolute value is less than width() + @param srcY row index whose absolute value is less than height() + @return true if pixels are copied to dst + + example: https://fiddle.skia.org/c/@Bitmap_readPixels_2 + */ + bool readPixels(const SkPixmap& dst, int srcX, int srcY) const; + + /** Copies a SkRect of pixels from SkBitmap to dst. Copy starts at (0, 0), and + does not exceed SkBitmap (width(), height()). + + dst specifies width, height, SkColorType, SkAlphaType, SkColorSpace, pixel storage, + and row bytes of destination. dst.rowBytes() specifics the gap from one destination + row to the next. Returns true if pixels are copied. Returns false if: + - dst pixel storage equals nullptr + - dst.rowBytes is less than SkImageInfo::minRowBytes() + - SkPixelRef is nullptr + + Pixels are copied only if pixel conversion is possible. If SkBitmap colorType() is + kGray_8_SkColorType, or kAlpha_8_SkColorType; dst SkColorType must match. + If SkBitmap colorType() is kGray_8_SkColorType, dst SkColorSpace must match. + If SkBitmap alphaType() is kOpaque_SkAlphaType, dst SkAlphaType must + match. If SkBitmap colorSpace() is nullptr, dst SkColorSpace must match. Returns + false if pixel conversion is not possible. + + @param dst destination SkPixmap: SkImageInfo, pixels, row bytes + @return true if pixels are copied to dst + */ + bool readPixels(const SkPixmap& dst) const { + return this->readPixels(dst, 0, 0); + } + + /** Copies a SkRect of pixels from src. Copy starts at (dstX, dstY), and does not exceed + (src.width(), src.height()). + + src specifies width, height, SkColorType, SkAlphaType, SkColorSpace, pixel storage, + and row bytes of source. src.rowBytes() specifics the gap from one source + row to the next. Returns true if pixels are copied. Returns false if: + - src pixel storage equals nullptr + - src.rowBytes is less than SkImageInfo::minRowBytes() + - SkPixelRef is nullptr + + Pixels are copied only if pixel conversion is possible. If SkBitmap colorType() is + kGray_8_SkColorType, or kAlpha_8_SkColorType; src SkColorType must match. + If SkBitmap colorType() is kGray_8_SkColorType, src SkColorSpace must match. + If SkBitmap alphaType() is kOpaque_SkAlphaType, src SkAlphaType must + match. If SkBitmap colorSpace() is nullptr, src SkColorSpace must match. Returns + false if pixel conversion is not possible. + + dstX and dstY may be negative to copy only top or left of source. Returns + false if width() or height() is zero or negative. + Returns false if abs(dstX) >= Bitmap width(), or if abs(dstY) >= Bitmap height(). + + @param src source SkPixmap: SkImageInfo, pixels, row bytes + @param dstX column index whose absolute value is less than width() + @param dstY row index whose absolute value is less than height() + @return true if src pixels are copied to SkBitmap + + example: https://fiddle.skia.org/c/@Bitmap_writePixels + */ + bool writePixels(const SkPixmap& src, int dstX, int dstY); + + /** Copies a SkRect of pixels from src. Copy starts at (0, 0), and does not exceed + (src.width(), src.height()). + + src specifies width, height, SkColorType, SkAlphaType, SkColorSpace, pixel storage, + and row bytes of source. src.rowBytes() specifics the gap from one source + row to the next. Returns true if pixels are copied. Returns false if: + - src pixel storage equals nullptr + - src.rowBytes is less than SkImageInfo::minRowBytes() + - SkPixelRef is nullptr + + Pixels are copied only if pixel conversion is possible. If SkBitmap colorType() is + kGray_8_SkColorType, or kAlpha_8_SkColorType; src SkColorType must match. + If SkBitmap colorType() is kGray_8_SkColorType, src SkColorSpace must match. + If SkBitmap alphaType() is kOpaque_SkAlphaType, src SkAlphaType must + match. If SkBitmap colorSpace() is nullptr, src SkColorSpace must match. Returns + false if pixel conversion is not possible. + + @param src source SkPixmap: SkImageInfo, pixels, row bytes + @return true if src pixels are copied to SkBitmap + */ + bool writePixels(const SkPixmap& src) { + return this->writePixels(src, 0, 0); + } + + /** Sets dst to alpha described by pixels. Returns false if dst cannot be written to + or dst pixels cannot be allocated. + + Uses HeapAllocator to reserve memory for dst SkPixelRef. + + @param dst holds SkPixelRef to fill with alpha layer + @return true if alpha layer was constructed in dst SkPixelRef + */ + bool extractAlpha(SkBitmap* dst) const { + return this->extractAlpha(dst, nullptr, nullptr, nullptr); + } + + /** Sets dst to alpha described by pixels. Returns false if dst cannot be written to + or dst pixels cannot be allocated. + + If paint is not nullptr and contains SkMaskFilter, SkMaskFilter + generates mask alpha from SkBitmap. Uses HeapAllocator to reserve memory for dst + SkPixelRef. Sets offset to top-left position for dst for alignment with SkBitmap; + (0, 0) unless SkMaskFilter generates mask. + + @param dst holds SkPixelRef to fill with alpha layer + @param paint holds optional SkMaskFilter; may be nullptr + @param offset top-left position for dst; may be nullptr + @return true if alpha layer was constructed in dst SkPixelRef + */ + bool extractAlpha(SkBitmap* dst, const SkPaint* paint, + SkIPoint* offset) const { + return this->extractAlpha(dst, paint, nullptr, offset); + } + + /** Sets dst to alpha described by pixels. Returns false if dst cannot be written to + or dst pixels cannot be allocated. + + If paint is not nullptr and contains SkMaskFilter, SkMaskFilter + generates mask alpha from SkBitmap. allocator may reference a custom allocation + class or be set to nullptr to use HeapAllocator. Sets offset to top-left + position for dst for alignment with SkBitmap; (0, 0) unless SkMaskFilter generates + mask. + + @param dst holds SkPixelRef to fill with alpha layer + @param paint holds optional SkMaskFilter; may be nullptr + @param allocator function to reserve memory for SkPixelRef; may be nullptr + @param offset top-left position for dst; may be nullptr + @return true if alpha layer was constructed in dst SkPixelRef + */ + bool extractAlpha(SkBitmap* dst, const SkPaint* paint, Allocator* allocator, + SkIPoint* offset) const; + + /** Copies SkBitmap pixel address, row bytes, and SkImageInfo to pixmap, if address + is available, and returns true. If pixel address is not available, return + false and leave pixmap unchanged. + + pixmap contents become invalid on any future change to SkBitmap. + + @param pixmap storage for pixel state if pixels are readable; otherwise, ignored + @return true if SkBitmap has direct access to pixels + + example: https://fiddle.skia.org/c/@Bitmap_peekPixels + */ + bool peekPixels(SkPixmap* pixmap) const; + + /** + * Make a shader with the specified tiling, matrix and sampling. + */ + sk_sp makeShader(SkTileMode tmx, SkTileMode tmy, const SkSamplingOptions&, + const SkMatrix* localMatrix = nullptr) const; + sk_sp makeShader(SkTileMode tmx, SkTileMode tmy, const SkSamplingOptions& sampling, + const SkMatrix& lm) const; + /** Defaults to clamp in both X and Y. */ + sk_sp makeShader(const SkSamplingOptions& sampling, const SkMatrix& lm) const; + sk_sp makeShader(const SkSamplingOptions& sampling, + const SkMatrix* lm = nullptr) const; + + /** + * Returns a new image from the bitmap. If the bitmap is marked immutable, this will + * share the pixel buffer. If not, it will make a copy of the pixels for the image. + */ + sk_sp asImage() const; + + /** Asserts if internal values are illegal or inconsistent. Only available if + SK_DEBUG is defined at compile time. + */ + SkDEBUGCODE(void validate() const;) + + /** \class SkBitmap::Allocator + Abstract subclass of HeapAllocator. + */ + class Allocator : public SkRefCnt { + public: + + /** Allocates the pixel memory for the bitmap, given its dimensions and + SkColorType. Returns true on success, where success means either setPixels() + or setPixelRef() was called. + + @param bitmap SkBitmap containing SkImageInfo as input, and SkPixelRef as output + @return true if SkPixelRef was allocated + */ + virtual bool allocPixelRef(SkBitmap* bitmap) = 0; + private: + using INHERITED = SkRefCnt; + }; + + /** \class SkBitmap::HeapAllocator + Subclass of SkBitmap::Allocator that returns a SkPixelRef that allocates its pixel + memory from the heap. This is the default SkBitmap::Allocator invoked by + allocPixels(). + */ + class HeapAllocator : public Allocator { + public: + + /** Allocates the pixel memory for the bitmap, given its dimensions and + SkColorType. Returns true on success, where success means either setPixels() + or setPixelRef() was called. + + @param bitmap SkBitmap containing SkImageInfo as input, and SkPixelRef as output + @return true if pixels are allocated + + example: https://fiddle.skia.org/c/@Bitmap_HeapAllocator_allocPixelRef + */ + bool allocPixelRef(SkBitmap* bitmap) override; + }; + +private: + sk_sp fPixelRef; + SkPixmap fPixmap; + sk_sp fMips; + + friend class SkImage_Raster; + friend class SkReadBuffer; // unflatten + friend class GrProxyProvider; // fMips +}; + +/////////////////////////////////////////////////////////////////////////////// + +inline uint32_t* SkBitmap::getAddr32(int x, int y) const { + SkASSERT(fPixmap.addr()); + return fPixmap.writable_addr32(x, y); +} + +inline uint16_t* SkBitmap::getAddr16(int x, int y) const { + SkASSERT(fPixmap.addr()); + return fPixmap.writable_addr16(x, y); +} + +inline uint8_t* SkBitmap::getAddr8(int x, int y) const { + SkASSERT(fPixmap.addr()); + return fPixmap.writable_addr8(x, y); +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBlendMode.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBlendMode.h new file mode 100644 index 00000000000..4abe9157620 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBlendMode.h @@ -0,0 +1,112 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkBlendMode_DEFINED +#define SkBlendMode_DEFINED + +#include "include/core/SkTypes.h" + +/** + * Blends are operators that take in two colors (source, destination) and return a new color. + * Many of these operate the same on all 4 components: red, green, blue, alpha. For these, + * we just document what happens to one component, rather than naming each one separately. + * + * Different SkColorTypes have different representations for color components: + * 8-bit: 0..255 + * 6-bit: 0..63 + * 5-bit: 0..31 + * 4-bit: 0..15 + * floats: 0...1 + * + * The documentation is expressed as if the component values are always 0..1 (floats). + * + * For brevity, the documentation uses the following abbreviations + * s : source + * d : destination + * sa : source alpha + * da : destination alpha + * + * Results are abbreviated + * r : if all 4 components are computed in the same manner + * ra : result alpha component + * rc : result "color": red, green, blue components + */ +enum class SkBlendMode { + kClear, //!< r = 0 + kSrc, //!< r = s + kDst, //!< r = d + kSrcOver, //!< r = s + (1-sa)*d + kDstOver, //!< r = d + (1-da)*s + kSrcIn, //!< r = s * da + kDstIn, //!< r = d * sa + kSrcOut, //!< r = s * (1-da) + kDstOut, //!< r = d * (1-sa) + kSrcATop, //!< r = s*da + d*(1-sa) + kDstATop, //!< r = d*sa + s*(1-da) + kXor, //!< r = s*(1-da) + d*(1-sa) + kPlus, //!< r = min(s + d, 1) + kModulate, //!< r = s*d + kScreen, //!< r = s + d - s*d + + kOverlay, //!< multiply or screen, depending on destination + kDarken, //!< rc = s + d - max(s*da, d*sa), ra = kSrcOver + kLighten, //!< rc = s + d - min(s*da, d*sa), ra = kSrcOver + kColorDodge, //!< brighten destination to reflect source + kColorBurn, //!< darken destination to reflect source + kHardLight, //!< multiply or screen, depending on source + kSoftLight, //!< lighten or darken, depending on source + kDifference, //!< rc = s + d - 2*(min(s*da, d*sa)), ra = kSrcOver + kExclusion, //!< rc = s + d - two(s*d), ra = kSrcOver + kMultiply, //!< r = s*(1-da) + d*(1-sa) + s*d + + kHue, //!< hue of source with saturation and luminosity of destination + kSaturation, //!< saturation of source with hue and luminosity of destination + kColor, //!< hue and saturation of source with luminosity of destination + kLuminosity, //!< luminosity of source with hue and saturation of destination + + kLastCoeffMode = kScreen, //!< last porter duff blend mode + kLastSeparableMode = kMultiply, //!< last blend mode operating separately on components + kLastMode = kLuminosity, //!< last valid value +}; + +static constexpr int kSkBlendModeCount = static_cast(SkBlendMode::kLastMode) + 1; + +/** + * For Porter-Duff SkBlendModes (those <= kLastCoeffMode), these coefficients describe the blend + * equation used. Coefficient-based blend modes specify an equation: + * ('dstCoeff' * dst + 'srcCoeff' * src), where the coefficient values are constants, functions of + * the src or dst alpha, or functions of the src or dst color. + */ +enum class SkBlendModeCoeff { + kZero, /** 0 */ + kOne, /** 1 */ + kSC, /** src color */ + kISC, /** inverse src color (i.e. 1 - sc) */ + kDC, /** dst color */ + kIDC, /** inverse dst color (i.e. 1 - dc) */ + kSA, /** src alpha */ + kISA, /** inverse src alpha (i.e. 1 - sa) */ + kDA, /** dst alpha */ + kIDA, /** inverse dst alpha (i.e. 1 - da) */ + + kCoeffCount +}; + +/** + * Returns true if 'mode' is a coefficient-based blend mode (<= kLastCoeffMode). If true is + * returned, the mode's src and dst coefficient functions are set in 'src' and 'dst'. + */ +SK_API bool SkBlendMode_AsCoeff(SkBlendMode mode, SkBlendModeCoeff* src, SkBlendModeCoeff* dst); + + +/** Returns name of blendMode as null-terminated C string. + + @return C string +*/ +SK_API const char* SkBlendMode_Name(SkBlendMode blendMode); + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBlender.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBlender.h new file mode 100644 index 00000000000..741c4614dfe --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBlender.h @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkBlender_DEFINED +#define SkBlender_DEFINED + +#include "include/core/SkBlendMode.h" +#include "include/core/SkFlattenable.h" + +/** + * SkBlender represents a custom blend function in the Skia pipeline. When an SkBlender is + * present in a paint, the SkBlendMode is ignored. A blender combines a source color (the + * result of our paint) and destination color (from the canvas) into a final color. + */ +class SK_API SkBlender : public SkFlattenable { +public: + /** + * Create a blender that implements the specified BlendMode. + */ + static sk_sp Mode(SkBlendMode mode); + +private: + SkBlender() = default; + friend class SkBlenderBase; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBlurTypes.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBlurTypes.h new file mode 100644 index 00000000000..f0dde10f25c --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkBlurTypes.h @@ -0,0 +1,20 @@ +/* + * Copyright 2014 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkBlurTypes_DEFINED +#define SkBlurTypes_DEFINED + +enum SkBlurStyle : int { + kNormal_SkBlurStyle, //!< fuzzy inside and outside + kSolid_SkBlurStyle, //!< solid inside, fuzzy outside + kOuter_SkBlurStyle, //!< nothing inside, fuzzy outside + kInner_SkBlurStyle, //!< fuzzy inside, nothing outside + + kLastEnum_SkBlurStyle = kInner_SkBlurStyle, +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCanvas.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCanvas.h new file mode 100644 index 00000000000..02ad95aa199 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCanvas.h @@ -0,0 +1,2686 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkCanvas_DEFINED +#define SkCanvas_DEFINED + +#include "include/core/SkArc.h" +#include "include/core/SkBlendMode.h" +#include "include/core/SkClipOp.h" +#include "include/core/SkColor.h" +#include "include/core/SkFontTypes.h" +#include "include/core/SkImageFilter.h" +#include "include/core/SkImageInfo.h" +#include "include/core/SkM44.h" +#include "include/core/SkMatrix.h" +#include "include/core/SkPaint.h" +#include "include/core/SkPoint.h" +#include "include/core/SkRasterHandleAllocator.h" +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkSamplingOptions.h" +#include "include/core/SkScalar.h" +#include "include/core/SkSize.h" +#include "include/core/SkSpan.h" +#include "include/core/SkString.h" +#include "include/core/SkSurfaceProps.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkCPUTypes.h" +#include "include/private/base/SkDeque.h" +#include "include/private/base/SkTArray.h" + +#include +#include +#include +#include + +#ifndef SK_SUPPORT_LEGACY_GETTOTALMATRIX +#define SK_SUPPORT_LEGACY_GETTOTALMATRIX +#endif + +namespace sktext { +class GlyphRunBuilder; +class GlyphRunList; +} + +class AutoLayerForImageFilter; +class GrRecordingContext; + +class SkBitmap; +class SkBlender; +class SkColorSpace; +class SkData; +class SkDevice; +class SkDrawable; +class SkFont; +class SkImage; +class SkMesh; +class SkPaintFilterCanvas; +class SkPath; +class SkPicture; +class SkPixmap; +class SkRRect; +class SkRegion; +class SkShader; +class SkSpecialImage; +class SkSurface; +class SkSurface_Base; +class SkTextBlob; +class SkVertices; +struct SkDrawShadowRec; +struct SkRSXform; + +template +class SkEnumBitMask; + +namespace skgpu::graphite { class Recorder; } +namespace sktext::gpu { class Slug; } +namespace SkRecords { class Draw; } + +/** \class SkCanvas + SkCanvas provides an interface for drawing, and how the drawing is clipped and transformed. + SkCanvas contains a stack of SkMatrix and clip values. + + SkCanvas and SkPaint together provide the state to draw into SkSurface or SkDevice. + Each SkCanvas draw call transforms the geometry of the object by the concatenation of all + SkMatrix values in the stack. The transformed geometry is clipped by the intersection + of all of clip values in the stack. The SkCanvas draw calls use SkPaint to supply drawing + state such as color, SkTypeface, text size, stroke width, SkShader and so on. + + To draw to a pixel-based destination, create raster surface or GPU surface. + Request SkCanvas from SkSurface to obtain the interface to draw. + SkCanvas generated by raster surface draws to memory visible to the CPU. + SkCanvas generated by GPU surface uses Vulkan or OpenGL to draw to the GPU. + + To draw to a document, obtain SkCanvas from SVG canvas, document PDF, or SkPictureRecorder. + SkDocument based SkCanvas and other SkCanvas subclasses reference SkDevice describing the + destination. + + SkCanvas can be constructed to draw to SkBitmap without first creating raster surface. + This approach may be deprecated in the future. +*/ +class SK_API SkCanvas { +public: + + /** Allocates raster SkCanvas that will draw directly into pixels. + + SkCanvas is returned if all parameters are valid. + Valid parameters include: + info dimensions are zero or positive; + info contains SkColorType and SkAlphaType supported by raster surface; + pixels is not nullptr; + rowBytes is zero or large enough to contain info width pixels of SkColorType. + + Pass zero for rowBytes to compute rowBytes from info width and size of pixel. + If rowBytes is greater than zero, it must be equal to or greater than + info width times bytes required for SkColorType. + + Pixel buffer size should be info height times computed rowBytes. + Pixels are not initialized. + To access pixels after drawing, call flush() or peekPixels(). + + @param info width, height, SkColorType, SkAlphaType, SkColorSpace, of raster surface; + width, or height, or both, may be zero + @param pixels pointer to destination pixels buffer + @param rowBytes interval from one SkSurface row to the next, or zero + @param props LCD striping orientation and setting for device independent fonts; + may be nullptr + @return SkCanvas if all parameters are valid; otherwise, nullptr + */ + static std::unique_ptr MakeRasterDirect(const SkImageInfo& info, void* pixels, + size_t rowBytes, + const SkSurfaceProps* props = nullptr); + + /** Allocates raster SkCanvas specified by inline image specification. Subsequent SkCanvas + calls draw into pixels. + SkColorType is set to kN32_SkColorType. + SkAlphaType is set to kPremul_SkAlphaType. + To access pixels after drawing, call flush() or peekPixels(). + + SkCanvas is returned if all parameters are valid. + Valid parameters include: + width and height are zero or positive; + pixels is not nullptr; + rowBytes is zero or large enough to contain width pixels of kN32_SkColorType. + + Pass zero for rowBytes to compute rowBytes from width and size of pixel. + If rowBytes is greater than zero, it must be equal to or greater than + width times bytes required for SkColorType. + + Pixel buffer size should be height times rowBytes. + + @param width pixel column count on raster surface created; must be zero or greater + @param height pixel row count on raster surface created; must be zero or greater + @param pixels pointer to destination pixels buffer; buffer size should be height + times rowBytes + @param rowBytes interval from one SkSurface row to the next, or zero + @return SkCanvas if all parameters are valid; otherwise, nullptr + */ + static std::unique_ptr MakeRasterDirectN32(int width, int height, SkPMColor* pixels, + size_t rowBytes) { + return MakeRasterDirect(SkImageInfo::MakeN32Premul(width, height), pixels, rowBytes); + } + + /** Creates an empty SkCanvas with no backing device or pixels, with + a width and height of zero. + + @return empty SkCanvas + + example: https://fiddle.skia.org/c/@Canvas_empty_constructor + */ + SkCanvas(); + + /** Creates SkCanvas of the specified dimensions without a SkSurface. + Used by subclasses with custom implementations for draw member functions. + + If props equals nullptr, SkSurfaceProps are created with + SkSurfaceProps::InitType settings, which choose the pixel striping + direction and order. Since a platform may dynamically change its direction when + the device is rotated, and since a platform may have multiple monitors with + different characteristics, it is best not to rely on this legacy behavior. + + @param width zero or greater + @param height zero or greater + @param props LCD striping orientation and setting for device independent fonts; + may be nullptr + @return SkCanvas placeholder with dimensions + + example: https://fiddle.skia.org/c/@Canvas_int_int_const_SkSurfaceProps_star + */ + SkCanvas(int width, int height, const SkSurfaceProps* props = nullptr); + + /** Private. For internal use only. + */ + explicit SkCanvas(sk_sp device); + + /** Constructs a canvas that draws into bitmap. + Sets kUnknown_SkPixelGeometry in constructed SkSurface. + + SkBitmap is copied so that subsequently editing bitmap will not affect + constructed SkCanvas. + + May be deprecated in the future. + + @param bitmap width, height, SkColorType, SkAlphaType, and pixel + storage of raster surface + @return SkCanvas that can be used to draw into bitmap + + example: https://fiddle.skia.org/c/@Canvas_copy_const_SkBitmap + */ + explicit SkCanvas(const SkBitmap& bitmap); + +#ifdef SK_BUILD_FOR_ANDROID_FRAMEWORK + /** Private. + */ + enum class ColorBehavior { + kLegacy, //!< placeholder + }; + + /** Private. For use by Android framework only. + + @param bitmap specifies a bitmap for the canvas to draw into + @param behavior specializes this constructor; value is unused + @return SkCanvas that can be used to draw into bitmap + */ + SkCanvas(const SkBitmap& bitmap, ColorBehavior behavior); +#endif + + /** Constructs a canvas that draws into bitmap. + Use props to match the device characteristics, like LCD striping. + + bitmap is copied so that subsequently editing bitmap will not affect + constructed SkCanvas. + + @param bitmap width, height, SkColorType, SkAlphaType, + and pixel storage of raster surface + @param props order and orientation of RGB striping; and whether to use + device independent fonts + @return SkCanvas that can be used to draw into bitmap + + example: https://fiddle.skia.org/c/@Canvas_const_SkBitmap_const_SkSurfaceProps + */ + SkCanvas(const SkBitmap& bitmap, const SkSurfaceProps& props); + + /** Draws saved layers, if any. + Frees up resources used by SkCanvas. + + example: https://fiddle.skia.org/c/@Canvas_destructor + */ + virtual ~SkCanvas(); + + /** Returns SkImageInfo for SkCanvas. If SkCanvas is not associated with raster surface or + GPU surface, returned SkColorType is set to kUnknown_SkColorType. + + @return dimensions and SkColorType of SkCanvas + + example: https://fiddle.skia.org/c/@Canvas_imageInfo + */ + SkImageInfo imageInfo() const; + + /** Copies SkSurfaceProps, if SkCanvas is associated with raster surface or + GPU surface, and returns true. Otherwise, returns false and leave props unchanged. + + @param props storage for writable SkSurfaceProps + @return true if SkSurfaceProps was copied + + DEPRECATED: Replace usage with getBaseProps() or getTopProps() + + example: https://fiddle.skia.org/c/@Canvas_getProps + */ + bool getProps(SkSurfaceProps* props) const; + + /** Returns the SkSurfaceProps associated with the canvas (i.e., at the base of the layer + stack). + + @return base SkSurfaceProps + */ + SkSurfaceProps getBaseProps() const; + + /** Returns the SkSurfaceProps associated with the canvas that are currently active (i.e., at + the top of the layer stack). This can differ from getBaseProps depending on the flags + passed to saveLayer (see SaveLayerFlagsSet). + + @return SkSurfaceProps active in the current/top layer + */ + SkSurfaceProps getTopProps() const; + + /** Gets the size of the base or root layer in global canvas coordinates. The + origin of the base layer is always (0,0). The area available for drawing may be + smaller (due to clipping or saveLayer). + + @return integral width and height of base layer + + example: https://fiddle.skia.org/c/@Canvas_getBaseLayerSize + */ + virtual SkISize getBaseLayerSize() const; + + /** Creates SkSurface matching info and props, and associates it with SkCanvas. + Returns nullptr if no match found. + + If props is nullptr, matches SkSurfaceProps in SkCanvas. If props is nullptr and SkCanvas + does not have SkSurfaceProps, creates SkSurface with default SkSurfaceProps. + + @param info width, height, SkColorType, SkAlphaType, and SkColorSpace + @param props SkSurfaceProps to match; may be nullptr to match SkCanvas + @return SkSurface matching info and props, or nullptr if no match is available + + example: https://fiddle.skia.org/c/@Canvas_makeSurface + */ + sk_sp makeSurface(const SkImageInfo& info, const SkSurfaceProps* props = nullptr); + + /** Returns Ganesh context of the GPU surface associated with SkCanvas. + + @return GPU context, if available; nullptr otherwise + + example: https://fiddle.skia.org/c/@Canvas_recordingContext + */ + virtual GrRecordingContext* recordingContext() const; + + + /** Returns Recorder for the GPU surface associated with SkCanvas. + + @return Recorder, if available; nullptr otherwise + */ + virtual skgpu::graphite::Recorder* recorder() const; + + /** Sometimes a canvas is owned by a surface. If it is, getSurface() will return a bare + * pointer to that surface, else this will return nullptr. + */ + SkSurface* getSurface() const; + + /** Returns the pixel base address, SkImageInfo, rowBytes, and origin if the pixels + can be read directly. The returned address is only valid + while SkCanvas is in scope and unchanged. Any SkCanvas call or SkSurface call + may invalidate the returned address and other returned values. + + If pixels are inaccessible, info, rowBytes, and origin are unchanged. + + @param info storage for writable pixels' SkImageInfo; may be nullptr + @param rowBytes storage for writable pixels' row bytes; may be nullptr + @param origin storage for SkCanvas top layer origin, its top-left corner; + may be nullptr + @return address of pixels, or nullptr if inaccessible + + example: https://fiddle.skia.org/c/@Canvas_accessTopLayerPixels_a + example: https://fiddle.skia.org/c/@Canvas_accessTopLayerPixels_b + */ + void* accessTopLayerPixels(SkImageInfo* info, size_t* rowBytes, SkIPoint* origin = nullptr); + + /** Returns custom context that tracks the SkMatrix and clip. + + Use SkRasterHandleAllocator to blend Skia drawing with custom drawing, typically performed + by the host platform user interface. The custom context returned is generated by + SkRasterHandleAllocator::MakeCanvas, which creates a custom canvas with raster storage for + the drawing destination. + + @return context of custom allocation + + example: https://fiddle.skia.org/c/@Canvas_accessTopRasterHandle + */ + SkRasterHandleAllocator::Handle accessTopRasterHandle() const; + + /** Returns true if SkCanvas has direct access to its pixels. + + Pixels are readable when SkDevice is raster. Pixels are not readable when SkCanvas + is returned from GPU surface, returned by SkDocument::beginPage, returned by + SkPictureRecorder::beginRecording, or SkCanvas is the base of a utility class + like DebugCanvas. + + pixmap is valid only while SkCanvas is in scope and unchanged. Any + SkCanvas or SkSurface call may invalidate the pixmap values. + + @param pixmap storage for pixel state if pixels are readable; otherwise, ignored + @return true if SkCanvas has direct access to pixels + + example: https://fiddle.skia.org/c/@Canvas_peekPixels + */ + bool peekPixels(SkPixmap* pixmap); + + /** Copies SkRect of pixels from SkCanvas into dstPixels. SkMatrix and clip are + ignored. + + Source SkRect corners are (srcX, srcY) and (imageInfo().width(), imageInfo().height()). + Destination SkRect corners are (0, 0) and (dstInfo.width(), dstInfo.height()). + Copies each readable pixel intersecting both rectangles, without scaling, + converting to dstInfo.colorType() and dstInfo.alphaType() if required. + + Pixels are readable when SkDevice is raster, or backed by a GPU. + Pixels are not readable when SkCanvas is returned by SkDocument::beginPage, + returned by SkPictureRecorder::beginRecording, or SkCanvas is the base of a utility + class like DebugCanvas. + + The destination pixel storage must be allocated by the caller. + + Pixel values are converted only if SkColorType and SkAlphaType + do not match. Only pixels within both source and destination rectangles + are copied. dstPixels contents outside SkRect intersection are unchanged. + + Pass negative values for srcX or srcY to offset pixels across or down destination. + + Does not copy, and returns false if: + - Source and destination rectangles do not intersect. + - SkCanvas pixels could not be converted to dstInfo.colorType() or dstInfo.alphaType(). + - SkCanvas pixels are not readable; for instance, SkCanvas is document-based. + - dstRowBytes is too small to contain one row of pixels. + + @param dstInfo width, height, SkColorType, and SkAlphaType of dstPixels + @param dstPixels storage for pixels; dstInfo.height() times dstRowBytes, or larger + @param dstRowBytes size of one destination row; dstInfo.width() times pixel size, or larger + @param srcX offset into readable pixels on x-axis; may be negative + @param srcY offset into readable pixels on y-axis; may be negative + @return true if pixels were copied + */ + bool readPixels(const SkImageInfo& dstInfo, void* dstPixels, size_t dstRowBytes, + int srcX, int srcY); + + /** Copies SkRect of pixels from SkCanvas into pixmap. SkMatrix and clip are + ignored. + + Source SkRect corners are (srcX, srcY) and (imageInfo().width(), imageInfo().height()). + Destination SkRect corners are (0, 0) and (pixmap.width(), pixmap.height()). + Copies each readable pixel intersecting both rectangles, without scaling, + converting to pixmap.colorType() and pixmap.alphaType() if required. + + Pixels are readable when SkDevice is raster, or backed by a GPU. + Pixels are not readable when SkCanvas is returned by SkDocument::beginPage, + returned by SkPictureRecorder::beginRecording, or SkCanvas is the base of a utility + class like DebugCanvas. + + Caller must allocate pixel storage in pixmap if needed. + + Pixel values are converted only if SkColorType and SkAlphaType + do not match. Only pixels within both source and destination SkRect + are copied. pixmap pixels contents outside SkRect intersection are unchanged. + + Pass negative values for srcX or srcY to offset pixels across or down pixmap. + + Does not copy, and returns false if: + - Source and destination rectangles do not intersect. + - SkCanvas pixels could not be converted to pixmap.colorType() or pixmap.alphaType(). + - SkCanvas pixels are not readable; for instance, SkCanvas is document-based. + - SkPixmap pixels could not be allocated. + - pixmap.rowBytes() is too small to contain one row of pixels. + + @param pixmap storage for pixels copied from SkCanvas + @param srcX offset into readable pixels on x-axis; may be negative + @param srcY offset into readable pixels on y-axis; may be negative + @return true if pixels were copied + + example: https://fiddle.skia.org/c/@Canvas_readPixels_2 + */ + bool readPixels(const SkPixmap& pixmap, int srcX, int srcY); + + /** Copies SkRect of pixels from SkCanvas into bitmap. SkMatrix and clip are + ignored. + + Source SkRect corners are (srcX, srcY) and (imageInfo().width(), imageInfo().height()). + Destination SkRect corners are (0, 0) and (bitmap.width(), bitmap.height()). + Copies each readable pixel intersecting both rectangles, without scaling, + converting to bitmap.colorType() and bitmap.alphaType() if required. + + Pixels are readable when SkDevice is raster, or backed by a GPU. + Pixels are not readable when SkCanvas is returned by SkDocument::beginPage, + returned by SkPictureRecorder::beginRecording, or SkCanvas is the base of a utility + class like DebugCanvas. + + Caller must allocate pixel storage in bitmap if needed. + + SkBitmap values are converted only if SkColorType and SkAlphaType + do not match. Only pixels within both source and destination rectangles + are copied. SkBitmap pixels outside SkRect intersection are unchanged. + + Pass negative values for srcX or srcY to offset pixels across or down bitmap. + + Does not copy, and returns false if: + - Source and destination rectangles do not intersect. + - SkCanvas pixels could not be converted to bitmap.colorType() or bitmap.alphaType(). + - SkCanvas pixels are not readable; for instance, SkCanvas is document-based. + - bitmap pixels could not be allocated. + - bitmap.rowBytes() is too small to contain one row of pixels. + + @param bitmap storage for pixels copied from SkCanvas + @param srcX offset into readable pixels on x-axis; may be negative + @param srcY offset into readable pixels on y-axis; may be negative + @return true if pixels were copied + + example: https://fiddle.skia.org/c/@Canvas_readPixels_3 + */ + bool readPixels(const SkBitmap& bitmap, int srcX, int srcY); + + /** Copies SkRect from pixels to SkCanvas. SkMatrix and clip are ignored. + Source SkRect corners are (0, 0) and (info.width(), info.height()). + Destination SkRect corners are (x, y) and + (imageInfo().width(), imageInfo().height()). + + Copies each readable pixel intersecting both rectangles, without scaling, + converting to imageInfo().colorType() and imageInfo().alphaType() if required. + + Pixels are writable when SkDevice is raster, or backed by a GPU. + Pixels are not writable when SkCanvas is returned by SkDocument::beginPage, + returned by SkPictureRecorder::beginRecording, or SkCanvas is the base of a utility + class like DebugCanvas. + + Pixel values are converted only if SkColorType and SkAlphaType + do not match. Only pixels within both source and destination rectangles + are copied. SkCanvas pixels outside SkRect intersection are unchanged. + + Pass negative values for x or y to offset pixels to the left or + above SkCanvas pixels. + + Does not copy, and returns false if: + - Source and destination rectangles do not intersect. + - pixels could not be converted to SkCanvas imageInfo().colorType() or + imageInfo().alphaType(). + - SkCanvas pixels are not writable; for instance, SkCanvas is document-based. + - rowBytes is too small to contain one row of pixels. + + @param info width, height, SkColorType, and SkAlphaType of pixels + @param pixels pixels to copy, of size info.height() times rowBytes, or larger + @param rowBytes size of one row of pixels; info.width() times pixel size, or larger + @param x offset into SkCanvas writable pixels on x-axis; may be negative + @param y offset into SkCanvas writable pixels on y-axis; may be negative + @return true if pixels were written to SkCanvas + + example: https://fiddle.skia.org/c/@Canvas_writePixels + */ + bool writePixels(const SkImageInfo& info, const void* pixels, size_t rowBytes, int x, int y); + + /** Copies SkRect from pixels to SkCanvas. SkMatrix and clip are ignored. + Source SkRect corners are (0, 0) and (bitmap.width(), bitmap.height()). + + Destination SkRect corners are (x, y) and + (imageInfo().width(), imageInfo().height()). + + Copies each readable pixel intersecting both rectangles, without scaling, + converting to imageInfo().colorType() and imageInfo().alphaType() if required. + + Pixels are writable when SkDevice is raster, or backed by a GPU. + Pixels are not writable when SkCanvas is returned by SkDocument::beginPage, + returned by SkPictureRecorder::beginRecording, or SkCanvas is the base of a utility + class like DebugCanvas. + + Pixel values are converted only if SkColorType and SkAlphaType + do not match. Only pixels within both source and destination rectangles + are copied. SkCanvas pixels outside SkRect intersection are unchanged. + + Pass negative values for x or y to offset pixels to the left or + above SkCanvas pixels. + + Does not copy, and returns false if: + - Source and destination rectangles do not intersect. + - bitmap does not have allocated pixels. + - bitmap pixels could not be converted to SkCanvas imageInfo().colorType() or + imageInfo().alphaType(). + - SkCanvas pixels are not writable; for instance, SkCanvas is document based. + - bitmap pixels are inaccessible; for instance, bitmap wraps a texture. + + @param bitmap contains pixels copied to SkCanvas + @param x offset into SkCanvas writable pixels on x-axis; may be negative + @param y offset into SkCanvas writable pixels on y-axis; may be negative + @return true if pixels were written to SkCanvas + + example: https://fiddle.skia.org/c/@Canvas_writePixels_2 + example: https://fiddle.skia.org/c/@State_Stack_a + example: https://fiddle.skia.org/c/@State_Stack_b + */ + bool writePixels(const SkBitmap& bitmap, int x, int y); + + /** Saves SkMatrix and clip. + Calling restore() discards changes to SkMatrix and clip, + restoring the SkMatrix and clip to their state when save() was called. + + SkMatrix may be changed by translate(), scale(), rotate(), skew(), concat(), setMatrix(), + and resetMatrix(). Clip may be changed by clipRect(), clipRRect(), clipPath(), clipRegion(). + + Saved SkCanvas state is put on a stack; multiple calls to save() should be balance + by an equal number of calls to restore(). + + Call restoreToCount() with result to restore this and subsequent saves. + + @return depth of saved stack + + example: https://fiddle.skia.org/c/@Canvas_save + */ + int save(); + + /** Saves SkMatrix and clip, and allocates a SkSurface for subsequent drawing. + Calling restore() discards changes to SkMatrix and clip, and draws the SkSurface. + + SkMatrix may be changed by translate(), scale(), rotate(), skew(), concat(), + setMatrix(), and resetMatrix(). Clip may be changed by clipRect(), clipRRect(), + clipPath(), clipRegion(). + + SkRect bounds suggests but does not define the SkSurface size. To clip drawing to + a specific rectangle, use clipRect(). + + Optional SkPaint paint applies alpha, SkColorFilter, SkImageFilter, and + SkBlendMode when restore() is called. + + Call restoreToCount() with returned value to restore this and subsequent saves. + + @param bounds hint to limit the size of the layer; may be nullptr + @param paint graphics state for layer; may be nullptr + @return depth of saved stack + + example: https://fiddle.skia.org/c/@Canvas_saveLayer + example: https://fiddle.skia.org/c/@Canvas_saveLayer_4 + */ + int saveLayer(const SkRect* bounds, const SkPaint* paint); + + /** Saves SkMatrix and clip, and allocates a SkSurface for subsequent drawing. + Calling restore() discards changes to SkMatrix and clip, and draws the SkSurface. + + SkMatrix may be changed by translate(), scale(), rotate(), skew(), concat(), + setMatrix(), and resetMatrix(). Clip may be changed by clipRect(), clipRRect(), + clipPath(), clipRegion(). + + SkRect bounds suggests but does not define the layer size. To clip drawing to + a specific rectangle, use clipRect(). + + Optional SkPaint paint applies alpha, SkColorFilter, SkImageFilter, and + SkBlendMode when restore() is called. + + Call restoreToCount() with returned value to restore this and subsequent saves. + + @param bounds hint to limit the size of layer; may be nullptr + @param paint graphics state for layer; may be nullptr + @return depth of saved stack + */ + int saveLayer(const SkRect& bounds, const SkPaint* paint) { + return this->saveLayer(&bounds, paint); + } + + /** Saves SkMatrix and clip, and allocates SkSurface for subsequent drawing. + + Calling restore() discards changes to SkMatrix and clip, + and blends layer with alpha opacity onto prior layer. + + SkMatrix may be changed by translate(), scale(), rotate(), skew(), concat(), + setMatrix(), and resetMatrix(). Clip may be changed by clipRect(), clipRRect(), + clipPath(), clipRegion(). + + SkRect bounds suggests but does not define layer size. To clip drawing to + a specific rectangle, use clipRect(). + + alpha of zero is fully transparent, 1.0f is fully opaque. + + Call restoreToCount() with returned value to restore this and subsequent saves. + + @param bounds hint to limit the size of layer; may be nullptr + @param alpha opacity of layer + @return depth of saved stack + + example: https://fiddle.skia.org/c/@Canvas_saveLayerAlpha + */ + int saveLayerAlphaf(const SkRect* bounds, float alpha); + // Helper that accepts an int between 0 and 255, and divides it by 255.0 + int saveLayerAlpha(const SkRect* bounds, U8CPU alpha) { + return this->saveLayerAlphaf(bounds, alpha * (1.0f / 255)); + } + + /** \enum SkCanvas::SaveLayerFlagsSet + SaveLayerFlags provides options that may be used in any combination in SaveLayerRec, + defining how layer allocated by saveLayer() operates. It may be set to zero, + kPreserveLCDText_SaveLayerFlag, kInitWithPrevious_SaveLayerFlag, or both flags. + */ + enum SaveLayerFlagsSet { + kPreserveLCDText_SaveLayerFlag = 1 << 1, + kInitWithPrevious_SaveLayerFlag = 1 << 2, //!< initializes with previous contents + // instead of matching previous layer's colortype, use F16 + kF16ColorType = 1 << 4, + }; + + using SaveLayerFlags = uint32_t; + using FilterSpan = SkSpan>; + static constexpr int kMaxFiltersPerLayer = 16; + + /** \struct SkCanvas::SaveLayerRec + SaveLayerRec contains the state used to create the layer. + */ + struct SaveLayerRec { + /** Sets fBounds, fPaint, and fBackdrop to nullptr. Clears fSaveLayerFlags. + + @return empty SaveLayerRec + */ + SaveLayerRec() {} + + /** Sets fBounds, fPaint, and fSaveLayerFlags; sets fBackdrop to nullptr. + + @param bounds layer dimensions; may be nullptr + @param paint applied to layer when overlaying prior layer; may be nullptr + @param saveLayerFlags SaveLayerRec options to modify layer + @return SaveLayerRec with empty fBackdrop + */ + SaveLayerRec(const SkRect* bounds, const SkPaint* paint, SaveLayerFlags saveLayerFlags = 0) + : SaveLayerRec(bounds, paint, nullptr, nullptr, 1.f, saveLayerFlags, /*filters=*/{}) {} + + /** Sets fBounds, fPaint, fBackdrop, and fSaveLayerFlags. + + @param bounds layer dimensions; may be nullptr + @param paint applied to layer when overlaying prior layer; + may be nullptr + @param backdrop If not null, this causes the current layer to be filtered by + backdrop, and then drawn into the new layer + (respecting the current clip). + If null, the new layer is initialized with transparent-black. + @param saveLayerFlags SaveLayerRec options to modify layer + @return SaveLayerRec fully specified + */ + SaveLayerRec(const SkRect* bounds, const SkPaint* paint, const SkImageFilter* backdrop, + SaveLayerFlags saveLayerFlags) + : SaveLayerRec(bounds, paint, backdrop, nullptr, 1.f, saveLayerFlags, /*filters=*/{}) {} + + /** Sets fBounds, fColorSpace, and fSaveLayerFlags. + + @param bounds layer dimensions; may be nullptr + @param paint applied to layer when overlaying prior layer; + may be nullptr + @param backdrop If not null, this causes the current layer to be filtered by + backdrop, and then drawn into the new layer + (respecting the current clip). + If null, the new layer is initialized with transparent-black. + @param colorSpace If not null, when the layer is restored, a color space + conversion will be applied from this color space to the + parent's color space. The restore paint and backdrop filters will + be applied in this color space. + If null, the new layer will inherit the color space from its + parent. + @param saveLayerFlags SaveLayerRec options to modify layer + @return SaveLayerRec fully specified + */ + SaveLayerRec(const SkRect* bounds, const SkPaint* paint, const SkImageFilter* backdrop, + const SkColorSpace* colorSpace, SaveLayerFlags saveLayerFlags) + : SaveLayerRec(bounds, paint, backdrop, colorSpace, 1.f, saveLayerFlags, /*filters=*/{}) {} + + /** hints at layer size limit */ + const SkRect* fBounds = nullptr; + + /** modifies overlay */ + const SkPaint* fPaint = nullptr; + + FilterSpan fFilters = {}; + + /** + * If not null, this triggers the same initialization behavior as setting + * kInitWithPrevious_SaveLayerFlag on fSaveLayerFlags: the current layer is copied into + * the new layer, rather than initializing the new layer with transparent-black. + * This is then filtered by fBackdrop (respecting the current clip). + */ + const SkImageFilter* fBackdrop = nullptr; + + /** + * If not null, this triggers a color space conversion when the layer is restored. It + * will be as if the layer's contents are drawn in this color space. Filters from + * fBackdrop and fPaint will be applied in this color space. + */ + const SkColorSpace* fColorSpace = nullptr; + + /** preserves LCD text, creates with prior layer contents */ + SaveLayerFlags fSaveLayerFlags = 0; + + private: + friend class SkCanvas; + friend class SkCanvasPriv; + + SaveLayerRec(const SkRect* bounds, + const SkPaint* paint, + const SkImageFilter* backdrop, + const SkColorSpace* colorSpace, + SkScalar backdropScale, + SaveLayerFlags saveLayerFlags, + FilterSpan filters) + : fBounds(bounds) + , fPaint(paint) + , fFilters(filters) + , fBackdrop(backdrop) + , fColorSpace(colorSpace) + , fSaveLayerFlags(saveLayerFlags) + , fExperimentalBackdropScale(backdropScale) { + // We only allow the paint's image filter or the side-car list of filters -- not both. + SkASSERT(fFilters.empty() || !paint || !paint->getImageFilter()); + // To keep things reasonable (during deserialization), we limit filter list size. + SkASSERT(fFilters.size() <= kMaxFiltersPerLayer); + } + + // Relative scale factor that the image content used to initialize the layer when the + // kInitFromPrevious flag or a backdrop filter is used. + SkScalar fExperimentalBackdropScale = 1.f; + }; + + /** Saves SkMatrix and clip, and allocates SkSurface for subsequent drawing. + + Calling restore() discards changes to SkMatrix and clip, + and blends SkSurface with alpha opacity onto the prior layer. + + SkMatrix may be changed by translate(), scale(), rotate(), skew(), concat(), + setMatrix(), and resetMatrix(). Clip may be changed by clipRect(), clipRRect(), + clipPath(), clipRegion(). + + SaveLayerRec contains the state used to create the layer. + + Call restoreToCount() with returned value to restore this and subsequent saves. + + @param layerRec layer state + @return depth of save state stack before this call was made. + + example: https://fiddle.skia.org/c/@Canvas_saveLayer_3 + */ + int saveLayer(const SaveLayerRec& layerRec); + + /** Removes changes to SkMatrix and clip since SkCanvas state was + last saved. The state is removed from the stack. + + Does nothing if the stack is empty. + + example: https://fiddle.skia.org/c/@AutoCanvasRestore_restore + + example: https://fiddle.skia.org/c/@Canvas_restore + */ + void restore(); + + /** Returns the number of saved states, each containing: SkMatrix and clip. + Equals the number of save() calls less the number of restore() calls plus one. + The save count of a new canvas is one. + + @return depth of save state stack + + example: https://fiddle.skia.org/c/@Canvas_getSaveCount + */ + int getSaveCount() const; + + /** Restores state to SkMatrix and clip values when save(), saveLayer(), + saveLayerPreserveLCDTextRequests(), or saveLayerAlpha() returned saveCount. + + Does nothing if saveCount is greater than state stack count. + Restores state to initial values if saveCount is less than or equal to one. + + @param saveCount depth of state stack to restore + + example: https://fiddle.skia.org/c/@Canvas_restoreToCount + */ + void restoreToCount(int saveCount); + + /** Translates SkMatrix by dx along the x-axis and dy along the y-axis. + + Mathematically, replaces SkMatrix with a translation matrix + premultiplied with SkMatrix. + + This has the effect of moving the drawing by (dx, dy) before transforming + the result with SkMatrix. + + @param dx distance to translate on x-axis + @param dy distance to translate on y-axis + + example: https://fiddle.skia.org/c/@Canvas_translate + */ + void translate(SkScalar dx, SkScalar dy); + + /** Scales SkMatrix by sx on the x-axis and sy on the y-axis. + + Mathematically, replaces SkMatrix with a scale matrix + premultiplied with SkMatrix. + + This has the effect of scaling the drawing by (sx, sy) before transforming + the result with SkMatrix. + + @param sx amount to scale on x-axis + @param sy amount to scale on y-axis + + example: https://fiddle.skia.org/c/@Canvas_scale + */ + void scale(SkScalar sx, SkScalar sy); + + /** Rotates SkMatrix by degrees. Positive degrees rotates clockwise. + + Mathematically, replaces SkMatrix with a rotation matrix + premultiplied with SkMatrix. + + This has the effect of rotating the drawing by degrees before transforming + the result with SkMatrix. + + @param degrees amount to rotate, in degrees + + example: https://fiddle.skia.org/c/@Canvas_rotate + */ + void rotate(SkScalar degrees); + + /** Rotates SkMatrix by degrees about a point at (px, py). Positive degrees rotates + clockwise. + + Mathematically, constructs a rotation matrix; premultiplies the rotation matrix by + a translation matrix; then replaces SkMatrix with the resulting matrix + premultiplied with SkMatrix. + + This has the effect of rotating the drawing about a given point before + transforming the result with SkMatrix. + + @param degrees amount to rotate, in degrees + @param px x-axis value of the point to rotate about + @param py y-axis value of the point to rotate about + + example: https://fiddle.skia.org/c/@Canvas_rotate_2 + */ + void rotate(SkScalar degrees, SkScalar px, SkScalar py); + + /** Skews SkMatrix by sx on the x-axis and sy on the y-axis. A positive value of sx + skews the drawing right as y-axis values increase; a positive value of sy skews + the drawing down as x-axis values increase. + + Mathematically, replaces SkMatrix with a skew matrix premultiplied with SkMatrix. + + This has the effect of skewing the drawing by (sx, sy) before transforming + the result with SkMatrix. + + @param sx amount to skew on x-axis + @param sy amount to skew on y-axis + + example: https://fiddle.skia.org/c/@Canvas_skew + */ + void skew(SkScalar sx, SkScalar sy); + + /** Replaces SkMatrix with matrix premultiplied with existing SkMatrix. + + This has the effect of transforming the drawn geometry by matrix, before + transforming the result with existing SkMatrix. + + @param matrix matrix to premultiply with existing SkMatrix + + example: https://fiddle.skia.org/c/@Canvas_concat + */ + void concat(const SkMatrix& matrix); + void concat(const SkM44&); + + /** Replaces SkMatrix with matrix. + Unlike concat(), any prior matrix state is overwritten. + + @param matrix matrix to copy, replacing existing SkMatrix + + example: https://fiddle.skia.org/c/@Canvas_setMatrix + */ + void setMatrix(const SkM44& matrix); + + // DEPRECATED -- use SkM44 version + void setMatrix(const SkMatrix& matrix); + + /** Sets SkMatrix to the identity matrix. + Any prior matrix state is overwritten. + + example: https://fiddle.skia.org/c/@Canvas_resetMatrix + */ + void resetMatrix(); + + /** Replaces clip with the intersection or difference of clip and rect, + with an aliased or anti-aliased clip edge. rect is transformed by SkMatrix + before it is combined with clip. + + @param rect SkRect to combine with clip + @param op SkClipOp to apply to clip + @param doAntiAlias true if clip is to be anti-aliased + + example: https://fiddle.skia.org/c/@Canvas_clipRect + */ + void clipRect(const SkRect& rect, SkClipOp op, bool doAntiAlias); + + /** Replaces clip with the intersection or difference of clip and rect. + Resulting clip is aliased; pixels are fully contained by the clip. + rect is transformed by SkMatrix before it is combined with clip. + + @param rect SkRect to combine with clip + @param op SkClipOp to apply to clip + */ + void clipRect(const SkRect& rect, SkClipOp op) { + this->clipRect(rect, op, false); + } + + /** Replaces clip with the intersection of clip and rect. + Resulting clip is aliased; pixels are fully contained by the clip. + rect is transformed by SkMatrix + before it is combined with clip. + + @param rect SkRect to combine with clip + @param doAntiAlias true if clip is to be anti-aliased + */ + void clipRect(const SkRect& rect, bool doAntiAlias = false) { + this->clipRect(rect, SkClipOp::kIntersect, doAntiAlias); + } + + void clipIRect(const SkIRect& irect, SkClipOp op = SkClipOp::kIntersect) { + this->clipRect(SkRect::Make(irect), op, false); + } + + /** Sets the maximum clip rectangle, which can be set by clipRect(), clipRRect() and + clipPath() and intersect the current clip with the specified rect. + The maximum clip affects only future clipping operations; it is not retroactive. + The clip restriction is not recorded in pictures. + + Pass an empty rect to disable maximum clip. + This private API is for use by Android framework only. + + DEPRECATED: Replace usage with SkAndroidFrameworkUtils::replaceClip() + + @param rect maximum allowed clip in device coordinates + */ + void androidFramework_setDeviceClipRestriction(const SkIRect& rect); + + /** Replaces clip with the intersection or difference of clip and rrect, + with an aliased or anti-aliased clip edge. + rrect is transformed by SkMatrix + before it is combined with clip. + + @param rrect SkRRect to combine with clip + @param op SkClipOp to apply to clip + @param doAntiAlias true if clip is to be anti-aliased + + example: https://fiddle.skia.org/c/@Canvas_clipRRect + */ + void clipRRect(const SkRRect& rrect, SkClipOp op, bool doAntiAlias); + + /** Replaces clip with the intersection or difference of clip and rrect. + Resulting clip is aliased; pixels are fully contained by the clip. + rrect is transformed by SkMatrix before it is combined with clip. + + @param rrect SkRRect to combine with clip + @param op SkClipOp to apply to clip + */ + void clipRRect(const SkRRect& rrect, SkClipOp op) { + this->clipRRect(rrect, op, false); + } + + /** Replaces clip with the intersection of clip and rrect, + with an aliased or anti-aliased clip edge. + rrect is transformed by SkMatrix before it is combined with clip. + + @param rrect SkRRect to combine with clip + @param doAntiAlias true if clip is to be anti-aliased + */ + void clipRRect(const SkRRect& rrect, bool doAntiAlias = false) { + this->clipRRect(rrect, SkClipOp::kIntersect, doAntiAlias); + } + + /** Replaces clip with the intersection or difference of clip and path, + with an aliased or anti-aliased clip edge. SkPath::FillType determines if path + describes the area inside or outside its contours; and if path contour overlaps + itself or another path contour, whether the overlaps form part of the area. + path is transformed by SkMatrix before it is combined with clip. + + @param path SkPath to combine with clip + @param op SkClipOp to apply to clip + @param doAntiAlias true if clip is to be anti-aliased + + example: https://fiddle.skia.org/c/@Canvas_clipPath + */ + void clipPath(const SkPath& path, SkClipOp op, bool doAntiAlias); + + /** Replaces clip with the intersection or difference of clip and path. + Resulting clip is aliased; pixels are fully contained by the clip. + SkPath::FillType determines if path + describes the area inside or outside its contours; and if path contour overlaps + itself or another path contour, whether the overlaps form part of the area. + path is transformed by SkMatrix + before it is combined with clip. + + @param path SkPath to combine with clip + @param op SkClipOp to apply to clip + */ + void clipPath(const SkPath& path, SkClipOp op) { + this->clipPath(path, op, false); + } + + /** Replaces clip with the intersection of clip and path. + Resulting clip is aliased; pixels are fully contained by the clip. + SkPath::FillType determines if path + describes the area inside or outside its contours; and if path contour overlaps + itself or another path contour, whether the overlaps form part of the area. + path is transformed by SkMatrix before it is combined with clip. + + @param path SkPath to combine with clip + @param doAntiAlias true if clip is to be anti-aliased + */ + void clipPath(const SkPath& path, bool doAntiAlias = false) { + this->clipPath(path, SkClipOp::kIntersect, doAntiAlias); + } + + void clipShader(sk_sp, SkClipOp = SkClipOp::kIntersect); + + /** Replaces clip with the intersection or difference of clip and SkRegion deviceRgn. + Resulting clip is aliased; pixels are fully contained by the clip. + deviceRgn is unaffected by SkMatrix. + + @param deviceRgn SkRegion to combine with clip + @param op SkClipOp to apply to clip + + example: https://fiddle.skia.org/c/@Canvas_clipRegion + */ + void clipRegion(const SkRegion& deviceRgn, SkClipOp op = SkClipOp::kIntersect); + + /** Returns true if SkRect rect, transformed by SkMatrix, can be quickly determined to be + outside of clip. May return false even though rect is outside of clip. + + Use to check if an area to be drawn is clipped out, to skip subsequent draw calls. + + @param rect SkRect to compare with clip + @return true if rect, transformed by SkMatrix, does not intersect clip + + example: https://fiddle.skia.org/c/@Canvas_quickReject + */ + bool quickReject(const SkRect& rect) const; + + /** Returns true if path, transformed by SkMatrix, can be quickly determined to be + outside of clip. May return false even though path is outside of clip. + + Use to check if an area to be drawn is clipped out, to skip subsequent draw calls. + + @param path SkPath to compare with clip + @return true if path, transformed by SkMatrix, does not intersect clip + + example: https://fiddle.skia.org/c/@Canvas_quickReject_2 + */ + bool quickReject(const SkPath& path) const; + + /** Returns bounds of clip, transformed by inverse of SkMatrix. If clip is empty, + return SkRect::MakeEmpty, where all SkRect sides equal zero. + + SkRect returned is outset by one to account for partial pixel coverage if clip + is anti-aliased. + + @return bounds of clip in local coordinates + + example: https://fiddle.skia.org/c/@Canvas_getLocalClipBounds + */ + SkRect getLocalClipBounds() const; + + /** Returns bounds of clip, transformed by inverse of SkMatrix. If clip is empty, + return false, and set bounds to SkRect::MakeEmpty, where all SkRect sides equal zero. + + bounds is outset by one to account for partial pixel coverage if clip + is anti-aliased. + + @param bounds SkRect of clip in local coordinates + @return true if clip bounds is not empty + */ + bool getLocalClipBounds(SkRect* bounds) const { + *bounds = this->getLocalClipBounds(); + return !bounds->isEmpty(); + } + + /** Returns SkIRect bounds of clip, unaffected by SkMatrix. If clip is empty, + return SkRect::MakeEmpty, where all SkRect sides equal zero. + + Unlike getLocalClipBounds(), returned SkIRect is not outset. + + @return bounds of clip in base device coordinates + + example: https://fiddle.skia.org/c/@Canvas_getDeviceClipBounds + */ + SkIRect getDeviceClipBounds() const; + + /** Returns SkIRect bounds of clip, unaffected by SkMatrix. If clip is empty, + return false, and set bounds to SkRect::MakeEmpty, where all SkRect sides equal zero. + + Unlike getLocalClipBounds(), bounds is not outset. + + @param bounds SkRect of clip in device coordinates + @return true if clip bounds is not empty + */ + bool getDeviceClipBounds(SkIRect* bounds) const { + *bounds = this->getDeviceClipBounds(); + return !bounds->isEmpty(); + } + + /** Fills clip with color color. + mode determines how ARGB is combined with destination. + + @param color unpremultiplied ARGB + @param mode SkBlendMode used to combine source color and destination + + example: https://fiddle.skia.org/c/@Canvas_drawColor + */ + void drawColor(SkColor color, SkBlendMode mode = SkBlendMode::kSrcOver) { + this->drawColor(SkColor4f::FromColor(color), mode); + } + + /** Fills clip with color color. + mode determines how ARGB is combined with destination. + + @param color SkColor4f representing unpremultiplied color. + @param mode SkBlendMode used to combine source color and destination + */ + void drawColor(const SkColor4f& color, SkBlendMode mode = SkBlendMode::kSrcOver); + + /** Fills clip with color color using SkBlendMode::kSrc. + This has the effect of replacing all pixels contained by clip with color. + + @param color unpremultiplied ARGB + */ + void clear(SkColor color) { + this->clear(SkColor4f::FromColor(color)); + } + + /** Fills clip with color color using SkBlendMode::kSrc. + This has the effect of replacing all pixels contained by clip with color. + + @param color SkColor4f representing unpremultiplied color. + */ + void clear(const SkColor4f& color) { + this->drawColor(color, SkBlendMode::kSrc); + } + + /** Makes SkCanvas contents undefined. Subsequent calls that read SkCanvas pixels, + such as drawing with SkBlendMode, return undefined results. discard() does + not change clip or SkMatrix. + + discard() may do nothing, depending on the implementation of SkSurface or SkDevice + that created SkCanvas. + + discard() allows optimized performance on subsequent draws by removing + cached data associated with SkSurface or SkDevice. + It is not necessary to call discard() once done with SkCanvas; + any cached data is deleted when owning SkSurface or SkDevice is deleted. + */ + void discard() { this->onDiscard(); } + + /** Fills clip with SkPaint paint. SkPaint components, SkShader, + SkColorFilter, SkImageFilter, and SkBlendMode affect drawing; + SkMaskFilter and SkPathEffect in paint are ignored. + + @param paint graphics state used to fill SkCanvas + + example: https://fiddle.skia.org/c/@Canvas_drawPaint + */ + void drawPaint(const SkPaint& paint); + + /** \enum SkCanvas::PointMode + Selects if an array of points are drawn as discrete points, as lines, or as + an open polygon. + */ + enum PointMode { + kPoints_PointMode, //!< draw each point separately + kLines_PointMode, //!< draw each pair of points as a line segment + kPolygon_PointMode, //!< draw the array of points as a open polygon + }; + + /** Draws pts using clip, SkMatrix and SkPaint paint. + count is the number of points; if count is less than one, has no effect. + mode may be one of: kPoints_PointMode, kLines_PointMode, or kPolygon_PointMode. + + If mode is kPoints_PointMode, the shape of point drawn depends on paint + SkPaint::Cap. If paint is set to SkPaint::kRound_Cap, each point draws a + circle of diameter SkPaint stroke width. If paint is set to SkPaint::kSquare_Cap + or SkPaint::kButt_Cap, each point draws a square of width and height + SkPaint stroke width. + + If mode is kLines_PointMode, each pair of points draws a line segment. + One line is drawn for every two points; each point is used once. If count is odd, + the final point is ignored. + + If mode is kPolygon_PointMode, each adjacent pair of points draws a line segment. + count minus one lines are drawn; the first and last point are used once. + + Each line segment respects paint SkPaint::Cap and SkPaint stroke width. + SkPaint::Style is ignored, as if were set to SkPaint::kStroke_Style. + + Always draws each element one at a time; is not affected by + SkPaint::Join, and unlike drawPath(), does not create a mask from all points + and lines before drawing. + + @param mode whether pts draws points or lines + @param count number of points in the array + @param pts array of points to draw + @param paint stroke, blend, color, and so on, used to draw + + example: https://fiddle.skia.org/c/@Canvas_drawPoints + */ + void drawPoints(PointMode mode, size_t count, const SkPoint pts[], const SkPaint& paint); + + /** Draws point at (x, y) using clip, SkMatrix and SkPaint paint. + + The shape of point drawn depends on paint SkPaint::Cap. + If paint is set to SkPaint::kRound_Cap, draw a circle of diameter + SkPaint stroke width. If paint is set to SkPaint::kSquare_Cap or SkPaint::kButt_Cap, + draw a square of width and height SkPaint stroke width. + SkPaint::Style is ignored, as if were set to SkPaint::kStroke_Style. + + @param x left edge of circle or square + @param y top edge of circle or square + @param paint stroke, blend, color, and so on, used to draw + + example: https://fiddle.skia.org/c/@Canvas_drawPoint + */ + void drawPoint(SkScalar x, SkScalar y, const SkPaint& paint); + + /** Draws point p using clip, SkMatrix and SkPaint paint. + + The shape of point drawn depends on paint SkPaint::Cap. + If paint is set to SkPaint::kRound_Cap, draw a circle of diameter + SkPaint stroke width. If paint is set to SkPaint::kSquare_Cap or SkPaint::kButt_Cap, + draw a square of width and height SkPaint stroke width. + SkPaint::Style is ignored, as if were set to SkPaint::kStroke_Style. + + @param p top-left edge of circle or square + @param paint stroke, blend, color, and so on, used to draw + */ + void drawPoint(SkPoint p, const SkPaint& paint) { + this->drawPoint(p.x(), p.y(), paint); + } + + /** Draws line segment from (x0, y0) to (x1, y1) using clip, SkMatrix, and SkPaint paint. + In paint: SkPaint stroke width describes the line thickness; + SkPaint::Cap draws the end rounded or square; + SkPaint::Style is ignored, as if were set to SkPaint::kStroke_Style. + + @param x0 start of line segment on x-axis + @param y0 start of line segment on y-axis + @param x1 end of line segment on x-axis + @param y1 end of line segment on y-axis + @param paint stroke, blend, color, and so on, used to draw + + example: https://fiddle.skia.org/c/@Canvas_drawLine + */ + void drawLine(SkScalar x0, SkScalar y0, SkScalar x1, SkScalar y1, const SkPaint& paint); + + /** Draws line segment from p0 to p1 using clip, SkMatrix, and SkPaint paint. + In paint: SkPaint stroke width describes the line thickness; + SkPaint::Cap draws the end rounded or square; + SkPaint::Style is ignored, as if were set to SkPaint::kStroke_Style. + + @param p0 start of line segment + @param p1 end of line segment + @param paint stroke, blend, color, and so on, used to draw + */ + void drawLine(SkPoint p0, SkPoint p1, const SkPaint& paint) { + this->drawLine(p0.x(), p0.y(), p1.x(), p1.y(), paint); + } + + /** Draws SkRect rect using clip, SkMatrix, and SkPaint paint. + In paint: SkPaint::Style determines if rectangle is stroked or filled; + if stroked, SkPaint stroke width describes the line thickness, and + SkPaint::Join draws the corners rounded or square. + + @param rect rectangle to draw + @param paint stroke or fill, blend, color, and so on, used to draw + + example: https://fiddle.skia.org/c/@Canvas_drawRect + */ + void drawRect(const SkRect& rect, const SkPaint& paint); + + /** Draws SkIRect rect using clip, SkMatrix, and SkPaint paint. + In paint: SkPaint::Style determines if rectangle is stroked or filled; + if stroked, SkPaint stroke width describes the line thickness, and + SkPaint::Join draws the corners rounded or square. + + @param rect rectangle to draw + @param paint stroke or fill, blend, color, and so on, used to draw + */ + void drawIRect(const SkIRect& rect, const SkPaint& paint) { + SkRect r; + r.set(rect); // promotes the ints to scalars + this->drawRect(r, paint); + } + + /** Draws SkRegion region using clip, SkMatrix, and SkPaint paint. + In paint: SkPaint::Style determines if rectangle is stroked or filled; + if stroked, SkPaint stroke width describes the line thickness, and + SkPaint::Join draws the corners rounded or square. + + @param region region to draw + @param paint SkPaint stroke or fill, blend, color, and so on, used to draw + + example: https://fiddle.skia.org/c/@Canvas_drawRegion + */ + void drawRegion(const SkRegion& region, const SkPaint& paint); + + /** Draws oval oval using clip, SkMatrix, and SkPaint. + In paint: SkPaint::Style determines if oval is stroked or filled; + if stroked, SkPaint stroke width describes the line thickness. + + @param oval SkRect bounds of oval + @param paint SkPaint stroke or fill, blend, color, and so on, used to draw + + example: https://fiddle.skia.org/c/@Canvas_drawOval + */ + void drawOval(const SkRect& oval, const SkPaint& paint); + + /** Draws SkRRect rrect using clip, SkMatrix, and SkPaint paint. + In paint: SkPaint::Style determines if rrect is stroked or filled; + if stroked, SkPaint stroke width describes the line thickness. + + rrect may represent a rectangle, circle, oval, uniformly rounded rectangle, or + may have any combination of positive non-square radii for the four corners. + + @param rrect SkRRect with up to eight corner radii to draw + @param paint SkPaint stroke or fill, blend, color, and so on, used to draw + + example: https://fiddle.skia.org/c/@Canvas_drawRRect + */ + void drawRRect(const SkRRect& rrect, const SkPaint& paint); + + /** Draws SkRRect outer and inner + using clip, SkMatrix, and SkPaint paint. + outer must contain inner or the drawing is undefined. + In paint: SkPaint::Style determines if SkRRect is stroked or filled; + if stroked, SkPaint stroke width describes the line thickness. + If stroked and SkRRect corner has zero length radii, SkPaint::Join can + draw corners rounded or square. + + GPU-backed platforms optimize drawing when both outer and inner are + concave and outer contains inner. These platforms may not be able to draw + SkPath built with identical data as fast. + + @param outer SkRRect outer bounds to draw + @param inner SkRRect inner bounds to draw + @param paint SkPaint stroke or fill, blend, color, and so on, used to draw + + example: https://fiddle.skia.org/c/@Canvas_drawDRRect_a + example: https://fiddle.skia.org/c/@Canvas_drawDRRect_b + */ + void drawDRRect(const SkRRect& outer, const SkRRect& inner, const SkPaint& paint); + + /** Draws circle at (cx, cy) with radius using clip, SkMatrix, and SkPaint paint. + If radius is zero or less, nothing is drawn. + In paint: SkPaint::Style determines if circle is stroked or filled; + if stroked, SkPaint stroke width describes the line thickness. + + @param cx circle center on the x-axis + @param cy circle center on the y-axis + @param radius half the diameter of circle + @param paint SkPaint stroke or fill, blend, color, and so on, used to draw + + example: https://fiddle.skia.org/c/@Canvas_drawCircle + */ + void drawCircle(SkScalar cx, SkScalar cy, SkScalar radius, const SkPaint& paint); + + /** Draws circle at center with radius using clip, SkMatrix, and SkPaint paint. + If radius is zero or less, nothing is drawn. + In paint: SkPaint::Style determines if circle is stroked or filled; + if stroked, SkPaint stroke width describes the line thickness. + + @param center circle center + @param radius half the diameter of circle + @param paint SkPaint stroke or fill, blend, color, and so on, used to draw + */ + void drawCircle(SkPoint center, SkScalar radius, const SkPaint& paint) { + this->drawCircle(center.x(), center.y(), radius, paint); + } + + /** Draws arc using clip, SkMatrix, and SkPaint paint. + + Arc is part of oval bounded by oval, sweeping from startAngle to startAngle plus + sweepAngle. startAngle and sweepAngle are in degrees. + + startAngle of zero places start point at the right middle edge of oval. + A positive sweepAngle places arc end point clockwise from start point; + a negative sweepAngle places arc end point counterclockwise from start point. + sweepAngle may exceed 360 degrees, a full circle. + If useCenter is true, draw a wedge that includes lines from oval + center to arc end points. If useCenter is false, draw arc between end points. + + If SkRect oval is empty or sweepAngle is zero, nothing is drawn. + + @param oval SkRect bounds of oval containing arc to draw + @param startAngle angle in degrees where arc begins + @param sweepAngle sweep angle in degrees; positive is clockwise + @param useCenter if true, include the center of the oval + @param paint SkPaint stroke or fill, blend, color, and so on, used to draw + */ + void drawArc(const SkRect& oval, SkScalar startAngle, SkScalar sweepAngle, + bool useCenter, const SkPaint& paint); + + /** Draws arc using clip, SkMatrix, and SkPaint paint. + + Arc is part of oval bounded by oval, sweeping from startAngle to startAngle plus + sweepAngle. startAngle and sweepAngle are in degrees. + + startAngle of zero places start point at the right middle edge of oval. + A positive sweepAngle places arc end point clockwise from start point; + a negative sweepAngle places arc end point counterclockwise from start point. + sweepAngle may exceed 360 degrees, a full circle. + If useCenter is true, draw a wedge that includes lines from oval + center to arc end points. If useCenter is false, draw arc between end points. + + If SkRect oval is empty or sweepAngle is zero, nothing is drawn. + + @param arc SkArc specifying oval, startAngle, sweepAngle, and arc-vs-wedge + @param paint SkPaint stroke or fill, blend, color, and so on, used to draw + */ + void drawArc(const SkArc& arc, const SkPaint& paint) { + this->drawArc(arc.fOval, arc.fStartAngle, arc.fSweepAngle, arc.isWedge(), paint); + } + + /** Draws SkRRect bounded by SkRect rect, with corner radii (rx, ry) using clip, + SkMatrix, and SkPaint paint. + + In paint: SkPaint::Style determines if SkRRect is stroked or filled; + if stroked, SkPaint stroke width describes the line thickness. + If rx or ry are less than zero, they are treated as if they are zero. + If rx plus ry exceeds rect width or rect height, radii are scaled down to fit. + If rx and ry are zero, SkRRect is drawn as SkRect and if stroked is affected by + SkPaint::Join. + + @param rect SkRect bounds of SkRRect to draw + @param rx axis length on x-axis of oval describing rounded corners + @param ry axis length on y-axis of oval describing rounded corners + @param paint stroke, blend, color, and so on, used to draw + + example: https://fiddle.skia.org/c/@Canvas_drawRoundRect + */ + void drawRoundRect(const SkRect& rect, SkScalar rx, SkScalar ry, const SkPaint& paint); + + /** Draws SkPath path using clip, SkMatrix, and SkPaint paint. + SkPath contains an array of path contour, each of which may be open or closed. + + In paint: SkPaint::Style determines if SkRRect is stroked or filled: + if filled, SkPath::FillType determines whether path contour describes inside or + outside of fill; if stroked, SkPaint stroke width describes the line thickness, + SkPaint::Cap describes line ends, and SkPaint::Join describes how + corners are drawn. + + @param path SkPath to draw + @param paint stroke, blend, color, and so on, used to draw + + example: https://fiddle.skia.org/c/@Canvas_drawPath + */ + void drawPath(const SkPath& path, const SkPaint& paint); + + void drawImage(const SkImage* image, SkScalar left, SkScalar top) { + this->drawImage(image, left, top, SkSamplingOptions(), nullptr); + } + void drawImage(const sk_sp& image, SkScalar left, SkScalar top) { + this->drawImage(image.get(), left, top, SkSamplingOptions(), nullptr); + } + + /** \enum SkCanvas::SrcRectConstraint + SrcRectConstraint controls the behavior at the edge of source SkRect, + provided to drawImageRect() when there is any filtering. If kStrict is set, + then extra code is used to ensure it never samples outside of the src-rect. + kStrict_SrcRectConstraint disables the use of mipmaps and anisotropic filtering. + */ + enum SrcRectConstraint { + kStrict_SrcRectConstraint, //!< sample only inside bounds; slower + kFast_SrcRectConstraint, //!< sample outside bounds; faster + }; + + void drawImage(const SkImage*, SkScalar x, SkScalar y, const SkSamplingOptions&, + const SkPaint* = nullptr); + void drawImage(const sk_sp& image, SkScalar x, SkScalar y, + const SkSamplingOptions& sampling, const SkPaint* paint = nullptr) { + this->drawImage(image.get(), x, y, sampling, paint); + } + void drawImageRect(const SkImage*, const SkRect& src, const SkRect& dst, + const SkSamplingOptions&, const SkPaint*, SrcRectConstraint); + void drawImageRect(const SkImage*, const SkRect& dst, const SkSamplingOptions&, + const SkPaint* = nullptr); + void drawImageRect(const sk_sp& image, const SkRect& src, const SkRect& dst, + const SkSamplingOptions& sampling, const SkPaint* paint, + SrcRectConstraint constraint) { + this->drawImageRect(image.get(), src, dst, sampling, paint, constraint); + } + void drawImageRect(const sk_sp& image, const SkRect& dst, + const SkSamplingOptions& sampling, const SkPaint* paint = nullptr) { + this->drawImageRect(image.get(), dst, sampling, paint); + } + + /** Draws SkImage image stretched proportionally to fit into SkRect dst. + SkIRect center divides the image into nine sections: four sides, four corners, and + the center. Corners are unmodified or scaled down proportionately if their sides + are larger than dst; center and four sides are scaled to fit remaining space, if any. + + Additionally transform draw using clip, SkMatrix, and optional SkPaint paint. + + If SkPaint paint is supplied, apply SkColorFilter, alpha, SkImageFilter, and + SkBlendMode. If image is kAlpha_8_SkColorType, apply SkShader. + If paint contains SkMaskFilter, generate mask from image bounds. + Any SkMaskFilter on paint is ignored as is paint anti-aliasing state. + + If generated mask extends beyond image bounds, replicate image edge colors, just + as SkShader made from SkImage::makeShader with SkShader::kClamp_TileMode set + replicates the image edge color when it samples outside of its bounds. + + @param image SkImage containing pixels, dimensions, and format + @param center SkIRect edge of image corners and sides + @param dst destination SkRect of image to draw to + @param filter what technique to use when sampling the image + @param paint SkPaint containing SkBlendMode, SkColorFilter, SkImageFilter, + and so on; or nullptr + */ + void drawImageNine(const SkImage* image, const SkIRect& center, const SkRect& dst, + SkFilterMode filter, const SkPaint* paint = nullptr); + + /** \struct SkCanvas::Lattice + SkCanvas::Lattice divides SkBitmap or SkImage into a rectangular grid. + Grid entries on even columns and even rows are fixed; these entries are + always drawn at their original size if the destination is large enough. + If the destination side is too small to hold the fixed entries, all fixed + entries are proportionately scaled down to fit. + The grid entries not on even columns and rows are scaled to fit the + remaining space, if any. + */ + struct Lattice { + + /** \enum SkCanvas::Lattice::RectType + Optional setting per rectangular grid entry to make it transparent, + or to fill the grid entry with a color. + */ + enum RectType : uint8_t { + kDefault = 0, //!< draws SkBitmap into lattice rectangle + kTransparent, //!< skips lattice rectangle by making it transparent + kFixedColor, //!< draws one of fColors into lattice rectangle + }; + + const int* fXDivs; //!< x-axis values dividing bitmap + const int* fYDivs; //!< y-axis values dividing bitmap + const RectType* fRectTypes; //!< array of fill types + int fXCount; //!< number of x-coordinates + int fYCount; //!< number of y-coordinates + const SkIRect* fBounds; //!< source bounds to draw from + const SkColor* fColors; //!< array of colors + }; + + /** Draws SkImage image stretched proportionally to fit into SkRect dst. + + SkCanvas::Lattice lattice divides image into a rectangular grid. + Each intersection of an even-numbered row and column is fixed; + fixed lattice elements never scale larger than their initial + size and shrink proportionately when all fixed elements exceed the bitmap + dimension. All other grid elements scale to fill the available space, if any. + + Additionally transform draw using clip, SkMatrix, and optional SkPaint paint. + + If SkPaint paint is supplied, apply SkColorFilter, alpha, SkImageFilter, and + SkBlendMode. If image is kAlpha_8_SkColorType, apply SkShader. + If paint contains SkMaskFilter, generate mask from image bounds. + Any SkMaskFilter on paint is ignored as is paint anti-aliasing state. + + If generated mask extends beyond bitmap bounds, replicate bitmap edge colors, + just as SkShader made from SkShader::MakeBitmapShader with + SkShader::kClamp_TileMode set replicates the bitmap edge color when it samples + outside of its bounds. + + @param image SkImage containing pixels, dimensions, and format + @param lattice division of bitmap into fixed and variable rectangles + @param dst destination SkRect of image to draw to + @param filter what technique to use when sampling the image + @param paint SkPaint containing SkBlendMode, SkColorFilter, SkImageFilter, + and so on; or nullptr + */ + void drawImageLattice(const SkImage* image, const Lattice& lattice, const SkRect& dst, + SkFilterMode filter, const SkPaint* paint = nullptr); + void drawImageLattice(const SkImage* image, const Lattice& lattice, const SkRect& dst) { + this->drawImageLattice(image, lattice, dst, SkFilterMode::kNearest, nullptr); + } + + /** + * Experimental. Controls anti-aliasing of each edge of images in an image-set. + */ + enum QuadAAFlags : unsigned { + kLeft_QuadAAFlag = 0b0001, + kTop_QuadAAFlag = 0b0010, + kRight_QuadAAFlag = 0b0100, + kBottom_QuadAAFlag = 0b1000, + + kNone_QuadAAFlags = 0b0000, + kAll_QuadAAFlags = 0b1111, + }; + + /** This is used by the experimental API below. */ + struct SK_API ImageSetEntry { + ImageSetEntry(sk_sp image, const SkRect& srcRect, const SkRect& dstRect, + int matrixIndex, float alpha, unsigned aaFlags, bool hasClip); + + ImageSetEntry(sk_sp image, const SkRect& srcRect, const SkRect& dstRect, + float alpha, unsigned aaFlags); + + ImageSetEntry(); + ~ImageSetEntry(); + ImageSetEntry(const ImageSetEntry&); + ImageSetEntry& operator=(const ImageSetEntry&); + + sk_sp fImage; + SkRect fSrcRect; + SkRect fDstRect; + int fMatrixIndex = -1; // Index into the preViewMatrices arg, or < 0 + float fAlpha = 1.f; + unsigned fAAFlags = kNone_QuadAAFlags; // QuadAAFlags + bool fHasClip = false; // True to use next 4 points in dstClip arg as quad + }; + + /** + * This is an experimental API for the SkiaRenderer Chromium project, and its API will surely + * evolve if it is not removed outright. + * + * This behaves very similarly to drawRect() combined with a clipPath() formed by clip + * quadrilateral. 'rect' and 'clip' are in the same coordinate space. If 'clip' is null, then it + * is as if the rectangle was not clipped (or, alternatively, clipped to itself). If not null, + * then it must provide 4 points. + * + * In addition to combining the draw and clipping into one operation, this function adds the + * additional capability of controlling each of the rectangle's edges anti-aliasing + * independently. The edges of the clip will respect the per-edge AA flags. It is required that + * 'clip' be contained inside 'rect'. In terms of mapping to edge labels, the 'clip' points + * should be ordered top-left, top-right, bottom-right, bottom-left so that the edge between [0] + * and [1] is "top", [1] and [2] is "right", [2] and [3] is "bottom", and [3] and [0] is "left". + * This ordering matches SkRect::toQuad(). + * + * This API only draws solid color, filled rectangles so it does not accept a full SkPaint. + */ + void experimental_DrawEdgeAAQuad(const SkRect& rect, const SkPoint clip[4], QuadAAFlags aaFlags, + const SkColor4f& color, SkBlendMode mode); + void experimental_DrawEdgeAAQuad(const SkRect& rect, const SkPoint clip[4], QuadAAFlags aaFlags, + SkColor color, SkBlendMode mode) { + this->experimental_DrawEdgeAAQuad(rect, clip, aaFlags, SkColor4f::FromColor(color), mode); + } + + /** + * This is an bulk variant of experimental_DrawEdgeAAQuad() that renders 'cnt' textured quads. + * For each entry, 'fDstRect' is rendered with its clip (determined by entry's 'fHasClip' and + * the current index in 'dstClip'). The entry's fImage is applied to the destination rectangle + * by sampling from 'fSrcRect' sub-image. The corners of 'fSrcRect' map to the corners of + * 'fDstRect', just like in drawImageRect(), and they will be properly interpolated when + * applying a clip. + * + * Like experimental_DrawEdgeAAQuad(), each entry can specify edge AA flags that apply to both + * the destination rect and its clip. + * + * If provided, the 'dstClips' array must have length equal 4 * the number of entries with + * fHasClip true. If 'dstClips' is null, every entry must have 'fHasClip' set to false. The + * destination clip coordinates will be read consecutively with the image set entries, advancing + * by 4 points every time an entry with fHasClip is passed. + * + * This entry point supports per-entry manipulations to the canvas's current matrix. If an + * entry provides 'fMatrixIndex' >= 0, it will be drawn as if the canvas's CTM was + * canvas->getTotalMatrix() * preViewMatrices[fMatrixIndex]. If 'fMatrixIndex' is less than 0, + * the pre-view matrix transform is implicitly the identity, so it will be drawn using just the + * current canvas matrix. The pre-view matrix modifies the canvas's view matrix, it does not + * affect the local coordinates of each entry. + * + * An optional paint may be provided, which supports the same subset of features usable with + * drawImageRect (i.e. assumed to be filled and no path effects). When a paint is provided, the + * image set is drawn as if each image used the applied paint independently, so each is affected + * by the image, color, and/or mask filter. + */ + void experimental_DrawEdgeAAImageSet(const ImageSetEntry imageSet[], int cnt, + const SkPoint dstClips[], const SkMatrix preViewMatrices[], + const SkSamplingOptions&, const SkPaint* paint = nullptr, + SrcRectConstraint constraint = kStrict_SrcRectConstraint); + + /** Draws text, with origin at (x, y), using clip, SkMatrix, SkFont font, + and SkPaint paint. + + When encoding is SkTextEncoding::kUTF8, SkTextEncoding::kUTF16, or + SkTextEncoding::kUTF32, this function uses the default + character-to-glyph mapping from the SkTypeface in font. It does not + perform typeface fallback for characters not found in the SkTypeface. + It does not perform kerning or other complex shaping; glyphs are + positioned based on their default advances. + + Text meaning depends on SkTextEncoding. + + Text size is affected by SkMatrix and SkFont text size. Default text + size is 12 point. + + All elements of paint: SkPathEffect, SkMaskFilter, SkShader, + SkColorFilter, and SkImageFilter; apply to text. By + default, draws filled black glyphs. + + @param text character code points or glyphs drawn + @param byteLength byte length of text array + @param encoding text encoding used in the text array + @param x start of text on x-axis + @param y start of text on y-axis + @param font typeface, text size and so, used to describe the text + @param paint blend, color, and so on, used to draw + */ + void drawSimpleText(const void* text, size_t byteLength, SkTextEncoding encoding, + SkScalar x, SkScalar y, const SkFont& font, const SkPaint& paint); + + /** Draws null terminated string, with origin at (x, y), using clip, SkMatrix, + SkFont font, and SkPaint paint. + + This function uses the default character-to-glyph mapping from the + SkTypeface in font. It does not perform typeface fallback for + characters not found in the SkTypeface. It does not perform kerning; + glyphs are positioned based on their default advances. + + String str is encoded as UTF-8. + + Text size is affected by SkMatrix and font text size. Default text + size is 12 point. + + All elements of paint: SkPathEffect, SkMaskFilter, SkShader, + SkColorFilter, and SkImageFilter; apply to text. By + default, draws filled black glyphs. + + @param str character code points drawn, + ending with a char value of zero + @param x start of string on x-axis + @param y start of string on y-axis + @param font typeface, text size and so, used to describe the text + @param paint blend, color, and so on, used to draw + */ + void drawString(const char str[], SkScalar x, SkScalar y, const SkFont& font, + const SkPaint& paint) { + this->drawSimpleText(str, strlen(str), SkTextEncoding::kUTF8, x, y, font, paint); + } + + /** Draws SkString, with origin at (x, y), using clip, SkMatrix, SkFont font, + and SkPaint paint. + + This function uses the default character-to-glyph mapping from the + SkTypeface in font. It does not perform typeface fallback for + characters not found in the SkTypeface. It does not perform kerning; + glyphs are positioned based on their default advances. + + SkString str is encoded as UTF-8. + + Text size is affected by SkMatrix and SkFont text size. Default text + size is 12 point. + + All elements of paint: SkPathEffect, SkMaskFilter, SkShader, + SkColorFilter, and SkImageFilter; apply to text. By + default, draws filled black glyphs. + + @param str character code points drawn, + ending with a char value of zero + @param x start of string on x-axis + @param y start of string on y-axis + @param font typeface, text size and so, used to describe the text + @param paint blend, color, and so on, used to draw + */ + void drawString(const SkString& str, SkScalar x, SkScalar y, const SkFont& font, + const SkPaint& paint) { + this->drawSimpleText(str.c_str(), str.size(), SkTextEncoding::kUTF8, x, y, font, paint); + } + + /** Draws count glyphs, at positions relative to origin styled with font and paint with + supporting utf8 and cluster information. + + This function draw glyphs at the given positions relative to the given origin. + It does not perform typeface fallback for glyphs not found in the SkTypeface in font. + + The drawing obeys the current transform matrix and clipping. + + All elements of paint: SkPathEffect, SkMaskFilter, SkShader, + SkColorFilter, and SkImageFilter; apply to text. By + default, draws filled black glyphs. + + @param count number of glyphs to draw + @param glyphs the array of glyphIDs to draw + @param positions where to draw each glyph relative to origin + @param clusters array of size count of cluster information + @param textByteCount size of the utf8text + @param utf8text utf8text supporting information for the glyphs + @param origin the origin of all the positions + @param font typeface, text size and so, used to describe the text + @param paint blend, color, and so on, used to draw + */ + void drawGlyphs(int count, const SkGlyphID glyphs[], const SkPoint positions[], + const uint32_t clusters[], int textByteCount, const char utf8text[], + SkPoint origin, const SkFont& font, const SkPaint& paint); + + /** Draws count glyphs, at positions relative to origin styled with font and paint. + + This function draw glyphs at the given positions relative to the given origin. + It does not perform typeface fallback for glyphs not found in the SkTypeface in font. + + The drawing obeys the current transform matrix and clipping. + + All elements of paint: SkPathEffect, SkMaskFilter, SkShader, + SkColorFilter, and SkImageFilter; apply to text. By + default, draws filled black glyphs. + + @param count number of glyphs to draw + @param glyphs the array of glyphIDs to draw + @param positions where to draw each glyph relative to origin + @param origin the origin of all the positions + @param font typeface, text size and so, used to describe the text + @param paint blend, color, and so on, used to draw + */ + void drawGlyphs(int count, const SkGlyphID glyphs[], const SkPoint positions[], + SkPoint origin, const SkFont& font, const SkPaint& paint); + + /** Draws count glyphs, at positions relative to origin styled with font and paint. + + This function draw glyphs using the given scaling and rotations. They are positioned + relative to the given origin. It does not perform typeface fallback for glyphs not found + in the SkTypeface in font. + + The drawing obeys the current transform matrix and clipping. + + All elements of paint: SkPathEffect, SkMaskFilter, SkShader, + SkColorFilter, and SkImageFilter; apply to text. By + default, draws filled black glyphs. + + @param count number of glyphs to draw + @param glyphs the array of glyphIDs to draw + @param xforms where to draw and orient each glyph + @param origin the origin of all the positions + @param font typeface, text size and so, used to describe the text + @param paint blend, color, and so on, used to draw + */ + void drawGlyphs(int count, const SkGlyphID glyphs[], const SkRSXform xforms[], + SkPoint origin, const SkFont& font, const SkPaint& paint); + + /** Draws SkTextBlob blob at (x, y), using clip, SkMatrix, and SkPaint paint. + + blob contains glyphs, their positions, and paint attributes specific to text: + SkTypeface, SkPaint text size, SkPaint text scale x, + SkPaint text skew x, SkPaint::Align, SkPaint::Hinting, anti-alias, SkPaint fake bold, + SkPaint font embedded bitmaps, SkPaint full hinting spacing, LCD text, SkPaint linear text, + and SkPaint subpixel text. + + SkTextEncoding must be set to SkTextEncoding::kGlyphID. + + Elements of paint: anti-alias, SkBlendMode, color including alpha, + SkColorFilter, SkPaint dither, SkMaskFilter, SkPathEffect, SkShader, and + SkPaint::Style; apply to blob. If SkPaint contains SkPaint::kStroke_Style: + SkPaint miter limit, SkPaint::Cap, SkPaint::Join, and SkPaint stroke width; + apply to SkPath created from blob. + + @param blob glyphs, positions, and their paints' text size, typeface, and so on + @param x horizontal offset applied to blob + @param y vertical offset applied to blob + @param paint blend, color, stroking, and so on, used to draw + + example: https://fiddle.skia.org/c/@Canvas_drawTextBlob + */ + void drawTextBlob(const SkTextBlob* blob, SkScalar x, SkScalar y, const SkPaint& paint); + + /** Draws SkTextBlob blob at (x, y), using clip, SkMatrix, and SkPaint paint. + + blob contains glyphs, their positions, and paint attributes specific to text: + SkTypeface, SkPaint text size, SkPaint text scale x, + SkPaint text skew x, SkPaint::Align, SkPaint::Hinting, anti-alias, SkPaint fake bold, + SkPaint font embedded bitmaps, SkPaint full hinting spacing, LCD text, SkPaint linear text, + and SkPaint subpixel text. + + SkTextEncoding must be set to SkTextEncoding::kGlyphID. + + Elements of paint: SkPathEffect, SkMaskFilter, SkShader, SkColorFilter, + and SkImageFilter; apply to blob. + + @param blob glyphs, positions, and their paints' text size, typeface, and so on + @param x horizontal offset applied to blob + @param y vertical offset applied to blob + @param paint blend, color, stroking, and so on, used to draw + */ + void drawTextBlob(const sk_sp& blob, SkScalar x, SkScalar y, const SkPaint& paint) { + this->drawTextBlob(blob.get(), x, y, paint); + } + + /** Draws SkPicture picture, using clip and SkMatrix. + Clip and SkMatrix are unchanged by picture contents, as if + save() was called before and restore() was called after drawPicture(). + + SkPicture records a series of draw commands for later playback. + + @param picture recorded drawing commands to play + */ + void drawPicture(const SkPicture* picture) { + this->drawPicture(picture, nullptr, nullptr); + } + + /** Draws SkPicture picture, using clip and SkMatrix. + Clip and SkMatrix are unchanged by picture contents, as if + save() was called before and restore() was called after drawPicture(). + + SkPicture records a series of draw commands for later playback. + + @param picture recorded drawing commands to play + */ + void drawPicture(const sk_sp& picture) { + this->drawPicture(picture.get()); + } + + /** Draws SkPicture picture, using clip and SkMatrix; transforming picture with + SkMatrix matrix, if provided; and use SkPaint paint alpha, SkColorFilter, + SkImageFilter, and SkBlendMode, if provided. + + If paint is non-null, then the picture is always drawn into a temporary layer before + actually landing on the canvas. Note that drawing into a layer can also change its + appearance if there are any non-associative blendModes inside any of the pictures elements. + + @param picture recorded drawing commands to play + @param matrix SkMatrix to rotate, scale, translate, and so on; may be nullptr + @param paint SkPaint to apply transparency, filtering, and so on; may be nullptr + + example: https://fiddle.skia.org/c/@Canvas_drawPicture_3 + */ + void drawPicture(const SkPicture* picture, const SkMatrix* matrix, const SkPaint* paint); + + /** Draws SkPicture picture, using clip and SkMatrix; transforming picture with + SkMatrix matrix, if provided; and use SkPaint paint alpha, SkColorFilter, + SkImageFilter, and SkBlendMode, if provided. + + If paint is non-null, then the picture is always drawn into a temporary layer before + actually landing on the canvas. Note that drawing into a layer can also change its + appearance if there are any non-associative blendModes inside any of the pictures elements. + + @param picture recorded drawing commands to play + @param matrix SkMatrix to rotate, scale, translate, and so on; may be nullptr + @param paint SkPaint to apply transparency, filtering, and so on; may be nullptr + */ + void drawPicture(const sk_sp& picture, const SkMatrix* matrix, + const SkPaint* paint) { + this->drawPicture(picture.get(), matrix, paint); + } + + /** Draws SkVertices vertices, a triangle mesh, using clip and SkMatrix. + If paint contains an SkShader and vertices does not contain texCoords, the shader + is mapped using the vertices' positions. + + SkBlendMode is ignored if SkVertices does not have colors. Otherwise, it combines + - the SkShader if SkPaint contains SkShader + - or the opaque SkPaint color if SkPaint does not contain SkShader + as the src of the blend and the interpolated vertex colors as the dst. + + SkMaskFilter, SkPathEffect, and antialiasing on SkPaint are ignored. + + @param vertices triangle mesh to draw + @param mode combines vertices' colors with SkShader if present or SkPaint opaque color + if not. Ignored if the vertices do not contain color. + @param paint specifies the SkShader, used as SkVertices texture, and SkColorFilter. + + example: https://fiddle.skia.org/c/@Canvas_drawVertices + */ + void drawVertices(const SkVertices* vertices, SkBlendMode mode, const SkPaint& paint); + + /** Draws SkVertices vertices, a triangle mesh, using clip and SkMatrix. + If paint contains an SkShader and vertices does not contain texCoords, the shader + is mapped using the vertices' positions. + + SkBlendMode is ignored if SkVertices does not have colors. Otherwise, it combines + - the SkShader if SkPaint contains SkShader + - or the opaque SkPaint color if SkPaint does not contain SkShader + as the src of the blend and the interpolated vertex colors as the dst. + + SkMaskFilter, SkPathEffect, and antialiasing on SkPaint are ignored. + + @param vertices triangle mesh to draw + @param mode combines vertices' colors with SkShader if present or SkPaint opaque color + if not. Ignored if the vertices do not contain color. + @param paint specifies the SkShader, used as SkVertices texture, may be nullptr + + example: https://fiddle.skia.org/c/@Canvas_drawVertices_2 + */ + void drawVertices(const sk_sp& vertices, SkBlendMode mode, const SkPaint& paint); + + /** + Experimental, under active development, and subject to change without notice. + + Draws a mesh using a user-defined specification (see SkMeshSpecification). Requires + a GPU backend or SkSL to be compiled in. + + SkBlender is ignored if SkMesh's specification does not output fragment shader color. + Otherwise, it combines + - the SkShader if SkPaint contains SkShader + - or the opaque SkPaint color if SkPaint does not contain SkShader + as the src of the blend and the mesh's fragment color as the dst. + + SkMaskFilter, SkPathEffect, and antialiasing on SkPaint are ignored. + + @param mesh the mesh vertices and compatible specification. + @param blender combines vertices colors with SkShader if present or SkPaint opaque color + if not. Ignored if the custom mesh does not output color. Defaults to + SkBlendMode::kModulate if nullptr. + @param paint specifies the SkShader, used as SkVertices texture, may be nullptr + */ + void drawMesh(const SkMesh& mesh, sk_sp blender, const SkPaint& paint); + + /** Draws a Coons patch: the interpolation of four cubics with shared corners, + associating a color, and optionally a texture SkPoint, with each corner. + + SkPoint array cubics specifies four SkPath cubic starting at the top-left corner, + in clockwise order, sharing every fourth point. The last SkPath cubic ends at the + first point. + + Color array color associates colors with corners in top-left, top-right, + bottom-right, bottom-left order. + + If paint contains SkShader, SkPoint array texCoords maps SkShader as texture to + corners in top-left, top-right, bottom-right, bottom-left order. If texCoords is + nullptr, SkShader is mapped using positions (derived from cubics). + + SkBlendMode is ignored if colors is null. Otherwise, it combines + - the SkShader if SkPaint contains SkShader + - or the opaque SkPaint color if SkPaint does not contain SkShader + as the src of the blend and the interpolated patch colors as the dst. + + SkMaskFilter, SkPathEffect, and antialiasing on SkPaint are ignored. + + @param cubics SkPath cubic array, sharing common points + @param colors color array, one for each corner + @param texCoords SkPoint array of texture coordinates, mapping SkShader to corners; + may be nullptr + @param mode combines patch's colors with SkShader if present or SkPaint opaque color + if not. Ignored if colors is null. + @param paint SkShader, SkColorFilter, SkBlendMode, used to draw + */ + void drawPatch(const SkPoint cubics[12], const SkColor colors[4], + const SkPoint texCoords[4], SkBlendMode mode, const SkPaint& paint); + + /** Draws a set of sprites from atlas, using clip, SkMatrix, and optional SkPaint paint. + paint uses anti-alias, alpha, SkColorFilter, SkImageFilter, and SkBlendMode + to draw, if present. For each entry in the array, SkRect tex locates sprite in + atlas, and SkRSXform xform transforms it into destination space. + + SkMaskFilter and SkPathEffect on paint are ignored. + + xform, tex, and colors if present, must contain count entries. + Optional colors are applied for each sprite using SkBlendMode mode, treating + sprite as source and colors as destination. + Optional cullRect is a conservative bounds of all transformed sprites. + If cullRect is outside of clip, canvas can skip drawing. + + If atlas is nullptr, this draws nothing. + + @param atlas SkImage containing sprites + @param xform SkRSXform mappings for sprites in atlas + @param tex SkRect locations of sprites in atlas + @param colors one per sprite, blended with sprite using SkBlendMode; may be nullptr + @param count number of sprites to draw + @param mode SkBlendMode combining colors and sprites + @param sampling SkSamplingOptions used when sampling from the atlas image + @param cullRect bounds of transformed sprites for efficient clipping; may be nullptr + @param paint SkColorFilter, SkImageFilter, SkBlendMode, and so on; may be nullptr + */ + void drawAtlas(const SkImage* atlas, const SkRSXform xform[], const SkRect tex[], + const SkColor colors[], int count, SkBlendMode mode, + const SkSamplingOptions& sampling, const SkRect* cullRect, const SkPaint* paint); + + /** Draws SkDrawable drawable using clip and SkMatrix, concatenated with + optional matrix. + + If SkCanvas has an asynchronous implementation, as is the case + when it is recording into SkPicture, then drawable will be referenced, + so that SkDrawable::draw() can be called when the operation is finalized. To force + immediate drawing, call SkDrawable::draw() instead. + + @param drawable custom struct encapsulating drawing commands + @param matrix transformation applied to drawing; may be nullptr + + example: https://fiddle.skia.org/c/@Canvas_drawDrawable + */ + void drawDrawable(SkDrawable* drawable, const SkMatrix* matrix = nullptr); + + /** Draws SkDrawable drawable using clip and SkMatrix, offset by (x, y). + + If SkCanvas has an asynchronous implementation, as is the case + when it is recording into SkPicture, then drawable will be referenced, + so that SkDrawable::draw() can be called when the operation is finalized. To force + immediate drawing, call SkDrawable::draw() instead. + + @param drawable custom struct encapsulating drawing commands + @param x offset into SkCanvas writable pixels on x-axis + @param y offset into SkCanvas writable pixels on y-axis + + example: https://fiddle.skia.org/c/@Canvas_drawDrawable_2 + */ + void drawDrawable(SkDrawable* drawable, SkScalar x, SkScalar y); + + /** Associates SkRect on SkCanvas with an annotation; a key-value pair, where the key is + a null-terminated UTF-8 string, and optional value is stored as SkData. + + Only some canvas implementations, such as recording to SkPicture, or drawing to + document PDF, use annotations. + + @param rect SkRect extent of canvas to annotate + @param key string used for lookup + @param value data holding value stored in annotation + + example: https://fiddle.skia.org/c/@Canvas_drawAnnotation_2 + */ + void drawAnnotation(const SkRect& rect, const char key[], SkData* value); + + /** Associates SkRect on SkCanvas when an annotation; a key-value pair, where the key is + a null-terminated UTF-8 string, and optional value is stored as SkData. + + Only some canvas implementations, such as recording to SkPicture, or drawing to + document PDF, use annotations. + + @param rect SkRect extent of canvas to annotate + @param key string used for lookup + @param value data holding value stored in annotation + */ + void drawAnnotation(const SkRect& rect, const char key[], const sk_sp& value) { + this->drawAnnotation(rect, key, value.get()); + } + + /** Returns true if clip is empty; that is, nothing will draw. + + May do work when called; it should not be called + more often than needed. However, once called, subsequent calls perform no + work until clip changes. + + @return true if clip is empty + + example: https://fiddle.skia.org/c/@Canvas_isClipEmpty + */ + virtual bool isClipEmpty() const; + + /** Returns true if clip is SkRect and not empty. + Returns false if the clip is empty, or if it is not SkRect. + + @return true if clip is SkRect and not empty + + example: https://fiddle.skia.org/c/@Canvas_isClipRect + */ + virtual bool isClipRect() const; + + /** Returns the current transform from local coordinates to the 'device', which for most + * purposes means pixels. + * + * @return transformation from local coordinates to device / pixels. + */ + SkM44 getLocalToDevice() const; + + /** + * Throws away the 3rd row and column in the matrix, so be warned. + */ + SkMatrix getLocalToDeviceAs3x3() const { + return this->getLocalToDevice().asM33(); + } + +#ifdef SK_SUPPORT_LEGACY_GETTOTALMATRIX + /** DEPRECATED + * Legacy version of getLocalToDevice(), which strips away any Z information, and + * just returns a 3x3 version. + * + * @return 3x3 version of getLocalToDevice() + * + * example: https://fiddle.skia.org/c/@Canvas_getTotalMatrix + * example: https://fiddle.skia.org/c/@Clip + */ + SkMatrix getTotalMatrix() const; +#endif + + /////////////////////////////////////////////////////////////////////////// + + /** + * Returns the global clip as a region. If the clip contains AA, then only the bounds + * of the clip may be returned. + */ + void temporary_internal_getRgnClip(SkRegion* region); + + void private_draw_shadow_rec(const SkPath&, const SkDrawShadowRec&); + + +protected: + // default impl defers to getDevice()->newSurface(info) + virtual sk_sp onNewSurface(const SkImageInfo& info, const SkSurfaceProps& props); + + // default impl defers to its device + virtual bool onPeekPixels(SkPixmap* pixmap); + virtual bool onAccessTopLayerPixels(SkPixmap* pixmap); + virtual SkImageInfo onImageInfo() const; + virtual bool onGetProps(SkSurfaceProps* props, bool top) const; + + // Subclass save/restore notifiers. + // Overriders should call the corresponding INHERITED method up the inheritance chain. + // getSaveLayerStrategy()'s return value may suppress full layer allocation. + enum SaveLayerStrategy { + kFullLayer_SaveLayerStrategy, + kNoLayer_SaveLayerStrategy, + }; + + virtual void willSave() {} + // Overriders should call the corresponding INHERITED method up the inheritance chain. + virtual SaveLayerStrategy getSaveLayerStrategy(const SaveLayerRec& ) { + return kFullLayer_SaveLayerStrategy; + } + + // returns true if we should actually perform the saveBehind, or false if we should just save. + virtual bool onDoSaveBehind(const SkRect*) { return true; } + virtual void willRestore() {} + virtual void didRestore() {} + + virtual void didConcat44(const SkM44&) {} + virtual void didSetM44(const SkM44&) {} + virtual void didTranslate(SkScalar, SkScalar) {} + virtual void didScale(SkScalar, SkScalar) {} + + // NOTE: If you are adding a new onDraw virtual to SkCanvas, PLEASE add an override to + // SkCanvasVirtualEnforcer (in SkCanvasVirtualEnforcer.h). This ensures that subclasses using + // that mechanism will be required to implement the new function. + virtual void onDrawPaint(const SkPaint& paint); + virtual void onDrawBehind(const SkPaint& paint); + virtual void onDrawRect(const SkRect& rect, const SkPaint& paint); + virtual void onDrawRRect(const SkRRect& rrect, const SkPaint& paint); + virtual void onDrawDRRect(const SkRRect& outer, const SkRRect& inner, const SkPaint& paint); + virtual void onDrawOval(const SkRect& rect, const SkPaint& paint); + virtual void onDrawArc(const SkRect& rect, SkScalar startAngle, SkScalar sweepAngle, + bool useCenter, const SkPaint& paint); + virtual void onDrawPath(const SkPath& path, const SkPaint& paint); + virtual void onDrawRegion(const SkRegion& region, const SkPaint& paint); + + virtual void onDrawTextBlob(const SkTextBlob* blob, SkScalar x, SkScalar y, + const SkPaint& paint); + + virtual void onDrawGlyphRunList(const sktext::GlyphRunList& glyphRunList, const SkPaint& paint); + + virtual void onDrawPatch(const SkPoint cubics[12], const SkColor colors[4], + const SkPoint texCoords[4], SkBlendMode mode, const SkPaint& paint); + virtual void onDrawPoints(PointMode mode, size_t count, const SkPoint pts[], + const SkPaint& paint); + + virtual void onDrawImage2(const SkImage*, SkScalar dx, SkScalar dy, const SkSamplingOptions&, + const SkPaint*); + virtual void onDrawImageRect2(const SkImage*, const SkRect& src, const SkRect& dst, + const SkSamplingOptions&, const SkPaint*, SrcRectConstraint); + virtual void onDrawImageLattice2(const SkImage*, const Lattice&, const SkRect& dst, + SkFilterMode, const SkPaint*); + virtual void onDrawAtlas2(const SkImage*, const SkRSXform[], const SkRect src[], + const SkColor[], int count, SkBlendMode, const SkSamplingOptions&, + const SkRect* cull, const SkPaint*); + virtual void onDrawEdgeAAImageSet2(const ImageSetEntry imageSet[], int count, + const SkPoint dstClips[], const SkMatrix preViewMatrices[], + const SkSamplingOptions&, const SkPaint*, + SrcRectConstraint); + + virtual void onDrawVerticesObject(const SkVertices* vertices, SkBlendMode mode, + const SkPaint& paint); + virtual void onDrawMesh(const SkMesh&, sk_sp, const SkPaint&); + virtual void onDrawAnnotation(const SkRect& rect, const char key[], SkData* value); + virtual void onDrawShadowRec(const SkPath&, const SkDrawShadowRec&); + + virtual void onDrawDrawable(SkDrawable* drawable, const SkMatrix* matrix); + virtual void onDrawPicture(const SkPicture* picture, const SkMatrix* matrix, + const SkPaint* paint); + + virtual void onDrawEdgeAAQuad(const SkRect& rect, const SkPoint clip[4], QuadAAFlags aaFlags, + const SkColor4f& color, SkBlendMode mode); + + enum ClipEdgeStyle { + kHard_ClipEdgeStyle, + kSoft_ClipEdgeStyle + }; + + virtual void onClipRect(const SkRect& rect, SkClipOp op, ClipEdgeStyle edgeStyle); + virtual void onClipRRect(const SkRRect& rrect, SkClipOp op, ClipEdgeStyle edgeStyle); + virtual void onClipPath(const SkPath& path, SkClipOp op, ClipEdgeStyle edgeStyle); + virtual void onClipShader(sk_sp, SkClipOp); + virtual void onClipRegion(const SkRegion& deviceRgn, SkClipOp op); + virtual void onResetClip(); + + virtual void onDiscard(); + + /** + */ + virtual sk_sp onConvertGlyphRunListToSlug( + const sktext::GlyphRunList& glyphRunList, const SkPaint& paint); + + /** + */ + virtual void onDrawSlug(const sktext::gpu::Slug* slug, const SkPaint& paint); + +private: + enum class PredrawFlags : unsigned { + kNone = 0, + kOpaqueShaderOverride = 1, // The paint's shader is overridden with an opaque image + kNonOpaqueShaderOverride = 2, // The paint's shader is overridden but is not opaque + kCheckForOverwrite = 4, // Check if the draw would overwrite the entire surface + kSkipMaskFilterAutoLayer = 8, // Do not apply mask filters in the AutoLayer + }; + // Inlined SK_DECL_BITMASK_OPS_FRIENDS to avoid including SkEnumBitMask.h + friend constexpr SkEnumBitMask operator|(PredrawFlags, PredrawFlags); + friend constexpr SkEnumBitMask operator&(PredrawFlags, PredrawFlags); + friend constexpr SkEnumBitMask operator^(PredrawFlags, PredrawFlags); + friend constexpr SkEnumBitMask operator~(PredrawFlags); + + // notify our surface (if we have one) that we are about to draw, so it + // can perform copy-on-write or invalidate any cached images + // returns false if the copy failed + [[nodiscard]] bool predrawNotify(bool willOverwritesEntireSurface = false); + [[nodiscard]] bool predrawNotify(const SkRect*, const SkPaint*, SkEnumBitMask); + + // call the appropriate predrawNotify and create a layer if needed. + std::optional aboutToDraw( + const SkPaint& paint, + const SkRect* rawBounds, + SkEnumBitMask flags); + std::optional aboutToDraw( + const SkPaint& paint, + const SkRect* rawBounds = nullptr); + + // The bottom-most device in the stack, only changed by init(). Image properties and the final + // canvas pixels are determined by this device. + SkDevice* rootDevice() const { + SkASSERT(fRootDevice); + return fRootDevice.get(); + } + + // The top-most device in the stack, will change within saveLayer()'s. All drawing and clipping + // operations should route to this device. + SkDevice* topDevice() const; + + // Canvases maintain a sparse stack of layers, where the top-most layer receives the drawing, + // clip, and matrix commands. There is a layer per call to saveLayer() using the + // kFullLayer_SaveLayerStrategy. + struct Layer { + sk_sp fDevice; + skia_private::STArray<1, sk_sp> fImageFilters; + SkPaint fPaint; + bool fIsCoverage; + bool fDiscard; + + Layer(sk_sp device, + FilterSpan imageFilters, + const SkPaint& paint, + bool isCoverage); + }; + + // Encapsulate state needed to restore from saveBehind() + struct BackImage { + // Out of line to avoid including SkSpecialImage.h + BackImage(sk_sp, SkIPoint); + BackImage(const BackImage&); + BackImage(BackImage&&); + BackImage& operator=(const BackImage&); + ~BackImage(); + + sk_sp fImage; + SkIPoint fLoc; + }; + + class MCRec { + public: + // If not null, this MCRec corresponds with the saveLayer() record that made the layer. + // The base "layer" is not stored here, since it is stored inline in SkCanvas and has no + // restoration behavior. + std::unique_ptr fLayer; + + // This points to the device of the top-most layer (which may be lower in the stack), or + // to the canvas's fRootDevice. The MCRec does not own the device. + SkDevice* fDevice; + + std::unique_ptr fBackImage; + SkM44 fMatrix; + int fDeferredSaveCount = 0; + + MCRec(SkDevice* device); + MCRec(const MCRec* prev); + ~MCRec(); + + void newLayer(sk_sp layerDevice, + FilterSpan filters, + const SkPaint& restorePaint, + bool layerIsCoverage); + + void reset(SkDevice* device); + }; + + // the first N recs that can fit here mean we won't call malloc + static constexpr int kMCRecSize = 96; // most recent measurement + static constexpr int kMCRecCount = 32; // common depth for save/restores + + intptr_t fMCRecStorage[kMCRecSize * kMCRecCount / sizeof(intptr_t)]; + + SkDeque fMCStack; + // points to top of stack + MCRec* fMCRec; + + // Installed via init() + sk_sp fRootDevice; + const SkSurfaceProps fProps; + + int fSaveCount; // value returned by getSaveCount() + + std::unique_ptr fAllocator; + + SkSurface_Base* fSurfaceBase; + SkSurface_Base* getSurfaceBase() const { return fSurfaceBase; } + void setSurfaceBase(SkSurface_Base* sb) { + fSurfaceBase = sb; + } + friend class SkSurface_Base; + friend class SkSurface_Ganesh; + + SkIRect fClipRestrictionRect = SkIRect::MakeEmpty(); + int fClipRestrictionSaveCount = -1; + + void doSave(); + void checkForDeferredSave(); + void internalSetMatrix(const SkM44&); + + friend class SkAndroidFrameworkUtils; + friend class SkCanvasPriv; // needs to expose android functions for testing outside android + friend class AutoLayerForImageFilter; + friend class SkSurface_Raster; // needs getDevice() + friend class SkNoDrawCanvas; // needs resetForNextPicture() + friend class SkNWayCanvas; + friend class SkPictureRecord; // predrawNotify (why does it need it? ) + friend class SkOverdrawCanvas; + friend class SkRasterHandleAllocator; + friend class SkRecords::Draw; + template + friend class SkTestCanvas; + +protected: + // For use by SkNoDrawCanvas (via SkCanvasVirtualEnforcer, which can't be a friend) + SkCanvas(const SkIRect& bounds); +private: + SkCanvas(const SkBitmap&, std::unique_ptr, + SkRasterHandleAllocator::Handle, const SkSurfaceProps* props); + + SkCanvas(SkCanvas&&) = delete; + SkCanvas(const SkCanvas&) = delete; + SkCanvas& operator=(SkCanvas&&) = delete; + SkCanvas& operator=(const SkCanvas&) = delete; + + friend class sktext::gpu::Slug; + friend class SkPicturePlayback; + /** + * Convert a SkTextBlob to a sktext::gpu::Slug using the current canvas state. + */ + sk_sp convertBlobToSlug(const SkTextBlob& blob, SkPoint origin, + const SkPaint& paint); + + /** + * Draw an sktext::gpu::Slug given the current canvas state. + */ + void drawSlug(const sktext::gpu::Slug* slug, const SkPaint& paint); + + /** Experimental + * Saves the specified subset of the current pixels in the current layer, + * and then clears those pixels to transparent black. + * Restores the pixels on restore() by drawing them in SkBlendMode::kDstOver. + * + * @param subset conservative bounds of the area to be saved / restored. + * @return depth of save state stack before this call was made. + */ + int only_axis_aligned_saveBehind(const SkRect* subset); + + /** + * Like drawPaint, but magically clipped to the most recent saveBehind buffer rectangle. + * If there is no active saveBehind, then this draws nothing. + */ + void drawClippedToSaveBehind(const SkPaint&); + + void resetForNextPicture(const SkIRect& bounds); + + // needs gettotalclip() + friend class SkCanvasStateUtils; + + void init(sk_sp); + + // All base onDrawX() functions should call this and skip drawing if it returns true. + // If 'matrix' is non-null, it maps the paint's fast bounds before checking for quick rejection + bool internalQuickReject(const SkRect& bounds, const SkPaint& paint, + const SkMatrix* matrix = nullptr); + + void internalDrawPaint(const SkPaint& paint); + void internalSaveLayer(const SaveLayerRec&, SaveLayerStrategy, bool coverageOnly=false); + void internalSaveBehind(const SkRect*); + + void internalConcat44(const SkM44&); + + // shared by save() and saveLayer() + void internalSave(); + void internalRestore(); + + enum class DeviceCompatibleWithFilter : bool { + // Check the src device's local-to-device matrix for compatibility with the filter, and if + // it is not compatible, introduce an intermediate image and transformation that allows the + // filter to be evaluated on the modified src content. + kUnknown = false, + // Assume that the src device's local-to-device matrix is compatible with the filter. + kYes = true + }; + /** + * Filters the contents of 'src' and draws the result into 'dst'. The filter is evaluated + * relative to the current canvas matrix, and src is drawn to dst using their relative transform + * 'paint' is applied after the filter and must not have a mask or image filter of its own. + * A null 'filter' behaves as if the identity filter were used. + * + * 'scaleFactor' is an extra uniform scale transform applied to downscale the 'src' image + * before any filtering, or as part of the copy, and is then drawn with 1/scaleFactor to 'dst'. + * Must be 1.0 if 'compat' is kYes (i.e. any scale factor has already been baked into the + * relative transforms between the devices). + */ + void internalDrawDeviceWithFilter(SkDevice* src, SkDevice* dst, + FilterSpan filters, const SkPaint& paint, + DeviceCompatibleWithFilter compat, + const SkColorInfo& filterColorInfo, + SkScalar scaleFactor = 1.f, + bool srcIsCoverageLayer = false); + + /* + * Returns true if drawing the specified rect (or all if it is null) with the specified + * paint (or default if null) would overwrite the entire root device of the canvas + * (i.e. the canvas' surface if it had one). + */ + bool wouldOverwriteEntireSurface(const SkRect*, const SkPaint*, + SkEnumBitMask) const; + + /** + * Returns true if the clip (for any active layer) contains antialiasing. + * If the clip is empty, this will return false. + */ + bool androidFramework_isClipAA() const; + + /** + * Reset the clip to be wide-open (modulo any separately specified device clip restriction). + * This operate within the save/restore clip stack so it can be undone by restoring to an + * earlier save point. + */ + void internal_private_resetClip(); + + virtual SkPaintFilterCanvas* internal_private_asPaintFilterCanvas() const { return nullptr; } + + // Keep track of the device clip bounds in the canvas' global space to reject draws before + // invoking the top-level device. + SkRect fQuickRejectBounds; + + // Compute the clip's bounds based on all clipped SkDevice's reported device bounds transformed + // into the canvas' global space. + SkRect computeDeviceClipBounds(bool outsetForAA=true) const; + + // Attempt to draw a rrect with an analytic blur. If the paint does not contain a blur, or the + // geometry can't be drawn with an analytic blur by the device, a layer is returned for a + // regular draw. If the draw succeeds or predrawNotify fails, nullopt is returned indicating + // that nothing further should be drawn. + std::optional attemptBlurredRRectDraw(const SkRRect&, + const SkPaint&, + SkEnumBitMask); + + class AutoUpdateQRBounds; + void validateClip() const; + + std::unique_ptr fScratchGlyphRunBuilder; +}; + +/** \class SkAutoCanvasRestore + Stack helper class calls SkCanvas::restoreToCount when SkAutoCanvasRestore + goes out of scope. Use this to guarantee that the canvas is restored to a known + state. +*/ +class SkAutoCanvasRestore { +public: + + /** Preserves SkCanvas::save() count. Optionally saves SkCanvas clip and SkCanvas matrix. + + @param canvas SkCanvas to guard + @param doSave call SkCanvas::save() + @return utility to restore SkCanvas state on destructor + */ + SkAutoCanvasRestore(SkCanvas* canvas, bool doSave) : fCanvas(canvas), fSaveCount(0) { + if (fCanvas) { + fSaveCount = canvas->getSaveCount(); + if (doSave) { + canvas->save(); + } + } + } + + /** Restores SkCanvas to saved state. Destructor is called when container goes out of + scope. + */ + ~SkAutoCanvasRestore() { + if (fCanvas) { + fCanvas->restoreToCount(fSaveCount); + } + } + + /** Restores SkCanvas to saved state immediately. Subsequent calls and + ~SkAutoCanvasRestore() have no effect. + */ + void restore() { + if (fCanvas) { + fCanvas->restoreToCount(fSaveCount); + fCanvas = nullptr; + } + } + +private: + SkCanvas* fCanvas; + int fSaveCount; + + SkAutoCanvasRestore(SkAutoCanvasRestore&&) = delete; + SkAutoCanvasRestore(const SkAutoCanvasRestore&) = delete; + SkAutoCanvasRestore& operator=(SkAutoCanvasRestore&&) = delete; + SkAutoCanvasRestore& operator=(const SkAutoCanvasRestore&) = delete; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCanvasVirtualEnforcer.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCanvasVirtualEnforcer.h new file mode 100644 index 00000000000..5086b4337d3 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCanvasVirtualEnforcer.h @@ -0,0 +1,61 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkCanvasVirtualEnforcer_DEFINED +#define SkCanvasVirtualEnforcer_DEFINED + +#include "include/core/SkCanvas.h" + +// If you would ordinarily want to inherit from Base (eg SkCanvas, SkNWayCanvas), instead +// inherit from SkCanvasVirtualEnforcer, which will make the build fail if you forget +// to override one of SkCanvas' key virtual hooks. +template +class SkCanvasVirtualEnforcer : public Base { +public: + using Base::Base; + +protected: + void onDrawPaint(const SkPaint& paint) override = 0; + void onDrawBehind(const SkPaint&) override {} // make zero after android updates + void onDrawRect(const SkRect& rect, const SkPaint& paint) override = 0; + void onDrawRRect(const SkRRect& rrect, const SkPaint& paint) override = 0; + void onDrawDRRect(const SkRRect& outer, const SkRRect& inner, + const SkPaint& paint) override = 0; + void onDrawOval(const SkRect& rect, const SkPaint& paint) override = 0; + void onDrawArc(const SkRect& rect, SkScalar startAngle, SkScalar sweepAngle, bool useCenter, + const SkPaint& paint) override = 0; + void onDrawPath(const SkPath& path, const SkPaint& paint) override = 0; + void onDrawRegion(const SkRegion& region, const SkPaint& paint) override = 0; + + void onDrawTextBlob(const SkTextBlob* blob, SkScalar x, SkScalar y, + const SkPaint& paint) override = 0; + + void onDrawPatch(const SkPoint cubics[12], const SkColor colors[4], + const SkPoint texCoords[4], SkBlendMode mode, + const SkPaint& paint) override = 0; + void onDrawPoints(SkCanvas::PointMode mode, size_t count, const SkPoint pts[], + const SkPaint& paint) override = 0; + +#ifdef SK_BUILD_FOR_ANDROID_FRAMEWORK + // This is under active development for Chrome and not used in Android. Hold off on adding + // implementations in Android's SkCanvas subclasses until this stabilizes. + void onDrawEdgeAAQuad(const SkRect& rect, const SkPoint clip[4], + SkCanvas::QuadAAFlags aaFlags, const SkColor4f& color, SkBlendMode mode) override {} +#else + void onDrawEdgeAAQuad(const SkRect& rect, const SkPoint clip[4], + SkCanvas::QuadAAFlags aaFlags, const SkColor4f& color, SkBlendMode mode) override = 0; +#endif + + void onDrawAnnotation(const SkRect& rect, const char key[], SkData* value) override = 0; + void onDrawShadowRec(const SkPath&, const SkDrawShadowRec&) override = 0; + + void onDrawDrawable(SkDrawable* drawable, const SkMatrix* matrix) override = 0; + void onDrawPicture(const SkPicture* picture, const SkMatrix* matrix, + const SkPaint* paint) override = 0; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCapabilities.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCapabilities.h new file mode 100644 index 00000000000..3053c575593 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCapabilities.h @@ -0,0 +1,39 @@ +/* + * Copyright 2022 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkCapabilities_DEFINED +#define SkCapabilities_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" +#include "include/sksl/SkSLVersion.h" + +namespace SkSL { struct ShaderCaps; } + +#if defined(SK_GRAPHITE) +namespace skgpu::graphite { class Caps; } +#endif + +class SK_API SkCapabilities : public SkRefCnt { +public: + static sk_sp RasterBackend(); + + SkSL::Version skslVersion() const { return fSkSLVersion; } + +protected: +#if defined(SK_GRAPHITE) + friend class skgpu::graphite::Caps; // for ctor +#endif + + SkCapabilities() = default; + + void initSkCaps(const SkSL::ShaderCaps*); + + SkSL::Version fSkSLVersion = SkSL::Version::k100; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkClipOp.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkClipOp.h new file mode 100644 index 00000000000..3da6c61131f --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkClipOp.h @@ -0,0 +1,19 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkClipOp_DEFINED +#define SkClipOp_DEFINED + +#include "include/core/SkTypes.h" + +enum class SkClipOp { + kDifference = 0, + kIntersect = 1, + kMax_EnumValue = kIntersect +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColor.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColor.h new file mode 100644 index 00000000000..33352c7a838 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColor.h @@ -0,0 +1,447 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkColor_DEFINED +#define SkColor_DEFINED + +#include "include/core/SkAlphaType.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkCPUTypes.h" + +#include +#include + +/** \file SkColor.h + + Types, consts, functions, and macros for colors. +*/ + +/** 8-bit type for an alpha value. 255 is 100% opaque, zero is 100% transparent. +*/ +typedef uint8_t SkAlpha; + +/** 32-bit ARGB color value, unpremultiplied. Color components are always in + a known order. This is different from SkPMColor, which has its bytes in a configuration + dependent order, to match the format of kBGRA_8888_SkColorType bitmaps. SkColor + is the type used to specify colors in SkPaint and in gradients. + + Color that is premultiplied has the same component values as color + that is unpremultiplied if alpha is 255, fully opaque, although may have the + component values in a different order. +*/ +typedef uint32_t SkColor; + +/** Returns color value from 8-bit component values. Asserts if SK_DEBUG is defined + if a, r, g, or b exceed 255. Since color is unpremultiplied, a may be smaller + than the largest of r, g, and b. + + @param a amount of alpha, from fully transparent (0) to fully opaque (255) + @param r amount of red, from no red (0) to full red (255) + @param g amount of green, from no green (0) to full green (255) + @param b amount of blue, from no blue (0) to full blue (255) + @return color and alpha, unpremultiplied +*/ +static constexpr inline SkColor SkColorSetARGB(U8CPU a, U8CPU r, U8CPU g, U8CPU b) { + return SkASSERT(a <= 255 && r <= 255 && g <= 255 && b <= 255), + (a << 24) | (r << 16) | (g << 8) | (b << 0); +} + +/** Returns color value from 8-bit component values, with alpha set + fully opaque to 255. +*/ +#define SkColorSetRGB(r, g, b) SkColorSetARGB(0xFF, r, g, b) + +/** Returns alpha byte from color value. +*/ +#define SkColorGetA(color) (((color) >> 24) & 0xFF) + +/** Returns red component of color, from zero to 255. +*/ +#define SkColorGetR(color) (((color) >> 16) & 0xFF) + +/** Returns green component of color, from zero to 255. +*/ +#define SkColorGetG(color) (((color) >> 8) & 0xFF) + +/** Returns blue component of color, from zero to 255. +*/ +#define SkColorGetB(color) (((color) >> 0) & 0xFF) + +/** Returns unpremultiplied color with red, blue, and green set from c; and alpha set + from a. Alpha component of c is ignored and is replaced by a in result. + + @param c packed RGB, eight bits per component + @param a alpha: transparent at zero, fully opaque at 255 + @return color with transparency +*/ +[[nodiscard]] static constexpr inline SkColor SkColorSetA(SkColor c, U8CPU a) { + return (c & 0x00FFFFFF) | (a << 24); +} + +/** Represents fully transparent SkAlpha value. SkAlpha ranges from zero, + fully transparent; to 255, fully opaque. +*/ +constexpr SkAlpha SK_AlphaTRANSPARENT = 0x00; + +/** Represents fully opaque SkAlpha value. SkAlpha ranges from zero, + fully transparent; to 255, fully opaque. +*/ +constexpr SkAlpha SK_AlphaOPAQUE = 0xFF; + +/** Represents fully transparent SkColor. May be used to initialize a destination + containing a mask or a non-rectangular image. +*/ +constexpr SkColor SK_ColorTRANSPARENT = SkColorSetARGB(0x00, 0x00, 0x00, 0x00); + +/** Represents fully opaque black. +*/ +constexpr SkColor SK_ColorBLACK = SkColorSetARGB(0xFF, 0x00, 0x00, 0x00); + +/** Represents fully opaque dark gray. + Note that SVG dark gray is equivalent to 0xFFA9A9A9. +*/ +constexpr SkColor SK_ColorDKGRAY = SkColorSetARGB(0xFF, 0x44, 0x44, 0x44); + +/** Represents fully opaque gray. + Note that HTML gray is equivalent to 0xFF808080. +*/ +constexpr SkColor SK_ColorGRAY = SkColorSetARGB(0xFF, 0x88, 0x88, 0x88); + +/** Represents fully opaque light gray. HTML silver is equivalent to 0xFFC0C0C0. + Note that SVG light gray is equivalent to 0xFFD3D3D3. +*/ +constexpr SkColor SK_ColorLTGRAY = SkColorSetARGB(0xFF, 0xCC, 0xCC, 0xCC); + +/** Represents fully opaque white. +*/ +constexpr SkColor SK_ColorWHITE = SkColorSetARGB(0xFF, 0xFF, 0xFF, 0xFF); + +/** Represents fully opaque red. +*/ +constexpr SkColor SK_ColorRED = SkColorSetARGB(0xFF, 0xFF, 0x00, 0x00); + +/** Represents fully opaque green. HTML lime is equivalent. + Note that HTML green is equivalent to 0xFF008000. +*/ +constexpr SkColor SK_ColorGREEN = SkColorSetARGB(0xFF, 0x00, 0xFF, 0x00); + +/** Represents fully opaque blue. +*/ +constexpr SkColor SK_ColorBLUE = SkColorSetARGB(0xFF, 0x00, 0x00, 0xFF); + +/** Represents fully opaque yellow. +*/ +constexpr SkColor SK_ColorYELLOW = SkColorSetARGB(0xFF, 0xFF, 0xFF, 0x00); + +/** Represents fully opaque cyan. HTML aqua is equivalent. +*/ +constexpr SkColor SK_ColorCYAN = SkColorSetARGB(0xFF, 0x00, 0xFF, 0xFF); + +/** Represents fully opaque magenta. HTML fuchsia is equivalent. +*/ +constexpr SkColor SK_ColorMAGENTA = SkColorSetARGB(0xFF, 0xFF, 0x00, 0xFF); + +/** Converts RGB to its HSV components. + hsv[0] contains hsv hue, a value from zero to less than 360. + hsv[1] contains hsv saturation, a value from zero to one. + hsv[2] contains hsv value, a value from zero to one. + + @param red red component value from zero to 255 + @param green green component value from zero to 255 + @param blue blue component value from zero to 255 + @param hsv three element array which holds the resulting HSV components +*/ +SK_API void SkRGBToHSV(U8CPU red, U8CPU green, U8CPU blue, SkScalar hsv[3]); + +/** Converts ARGB to its HSV components. Alpha in ARGB is ignored. + hsv[0] contains hsv hue, and is assigned a value from zero to less than 360. + hsv[1] contains hsv saturation, a value from zero to one. + hsv[2] contains hsv value, a value from zero to one. + + @param color ARGB color to convert + @param hsv three element array which holds the resulting HSV components +*/ +static inline void SkColorToHSV(SkColor color, SkScalar hsv[3]) { + SkRGBToHSV(SkColorGetR(color), SkColorGetG(color), SkColorGetB(color), hsv); +} + +/** Converts HSV components to an ARGB color. Alpha is passed through unchanged. + hsv[0] represents hsv hue, an angle from zero to less than 360. + hsv[1] represents hsv saturation, and varies from zero to one. + hsv[2] represents hsv value, and varies from zero to one. + + Out of range hsv values are pinned. + + @param alpha alpha component of the returned ARGB color + @param hsv three element array which holds the input HSV components + @return ARGB equivalent to HSV +*/ +SK_API SkColor SkHSVToColor(U8CPU alpha, const SkScalar hsv[3]); + +/** Converts HSV components to an ARGB color. Alpha is set to 255. + hsv[0] represents hsv hue, an angle from zero to less than 360. + hsv[1] represents hsv saturation, and varies from zero to one. + hsv[2] represents hsv value, and varies from zero to one. + + Out of range hsv values are pinned. + + @param hsv three element array which holds the input HSV components + @return RGB equivalent to HSV +*/ +static inline SkColor SkHSVToColor(const SkScalar hsv[3]) { + return SkHSVToColor(0xFF, hsv); +} + +/** 32-bit ARGB color value, premultiplied. The byte order for this value is + configuration dependent, matching the format of kBGRA_8888_SkColorType bitmaps. + This is different from SkColor, which is unpremultiplied, and is always in the + same byte order. +*/ +typedef uint32_t SkPMColor; + +/** Returns a SkPMColor value from unpremultiplied 8-bit component values. + + @param a amount of alpha, from fully transparent (0) to fully opaque (255) + @param r amount of red, from no red (0) to full red (255) + @param g amount of green, from no green (0) to full green (255) + @param b amount of blue, from no blue (0) to full blue (255) + @return premultiplied color +*/ +SK_API SkPMColor SkPreMultiplyARGB(U8CPU a, U8CPU r, U8CPU g, U8CPU b); + +/** Returns pmcolor closest to color c. Multiplies c RGB components by the c alpha, + and arranges the bytes to match the format of kN32_SkColorType. + + @param c unpremultiplied ARGB color + @return premultiplied color +*/ +SK_API SkPMColor SkPreMultiplyColor(SkColor c); + +/** \enum SkColorChannel + Describes different color channels one can manipulate +*/ +enum class SkColorChannel { + kR, // the red channel + kG, // the green channel + kB, // the blue channel + kA, // the alpha channel + + kLastEnum = kA, +}; + +/** Used to represent the channels available in a color type or texture format as a mask. */ +enum SkColorChannelFlag : uint32_t { + kRed_SkColorChannelFlag = 1 << static_cast(SkColorChannel::kR), + kGreen_SkColorChannelFlag = 1 << static_cast(SkColorChannel::kG), + kBlue_SkColorChannelFlag = 1 << static_cast(SkColorChannel::kB), + kAlpha_SkColorChannelFlag = 1 << static_cast(SkColorChannel::kA), + kGray_SkColorChannelFlag = 0x10, + // Convenience values + kGrayAlpha_SkColorChannelFlags = kGray_SkColorChannelFlag | kAlpha_SkColorChannelFlag, + kRG_SkColorChannelFlags = kRed_SkColorChannelFlag | kGreen_SkColorChannelFlag, + kRGB_SkColorChannelFlags = kRG_SkColorChannelFlags | kBlue_SkColorChannelFlag, + kRGBA_SkColorChannelFlags = kRGB_SkColorChannelFlags | kAlpha_SkColorChannelFlag, +}; +static_assert(0 == (kGray_SkColorChannelFlag & kRGBA_SkColorChannelFlags), "bitfield conflict"); + +/** \struct SkRGBA4f + RGBA color value, holding four floating point components. Color components are always in + a known order. kAT determines if the SkRGBA4f's R, G, and B components are premultiplied + by alpha or not. + + Skia's public API always uses unpremultiplied colors, which can be stored as + SkRGBA4f. For convenience, this type can also be referred to + as SkColor4f. +*/ +template +struct SkRGBA4f { + float fR; //!< red component + float fG; //!< green component + float fB; //!< blue component + float fA; //!< alpha component + + /** Compares SkRGBA4f with other, and returns true if all components are equal. + + @param other SkRGBA4f to compare + @return true if SkRGBA4f equals other + */ + bool operator==(const SkRGBA4f& other) const { + return fA == other.fA && fR == other.fR && fG == other.fG && fB == other.fB; + } + + /** Compares SkRGBA4f with other, and returns true if not all components are equal. + + @param other SkRGBA4f to compare + @return true if SkRGBA4f is not equal to other + */ + bool operator!=(const SkRGBA4f& other) const { + return !(*this == other); + } + + /** Returns SkRGBA4f multiplied by scale. + + @param scale value to multiply by + @return SkRGBA4f as (fR * scale, fG * scale, fB * scale, fA * scale) + */ + SkRGBA4f operator*(float scale) const { + return { fR * scale, fG * scale, fB * scale, fA * scale }; + } + + /** Returns SkRGBA4f multiplied component-wise by scale. + + @param scale SkRGBA4f to multiply by + @return SkRGBA4f as (fR * scale.fR, fG * scale.fG, fB * scale.fB, fA * scale.fA) + */ + SkRGBA4f operator*(const SkRGBA4f& scale) const { + return { fR * scale.fR, fG * scale.fG, fB * scale.fB, fA * scale.fA }; + } + + /** Returns a pointer to components of SkRGBA4f, for array access. + + @return pointer to array [fR, fG, fB, fA] + */ + const float* vec() const { return &fR; } + + /** Returns a pointer to components of SkRGBA4f, for array access. + + @return pointer to array [fR, fG, fB, fA] + */ + float* vec() { return &fR; } + + /** As a std::array */ + std::array array() const { return {fR, fG, fB, fA}; } + + /** Returns one component. Asserts if index is out of range and SK_DEBUG is defined. + + @param index one of: 0 (fR), 1 (fG), 2 (fB), 3 (fA) + @return value corresponding to index + */ + float operator[](int index) const { + SkASSERT(index >= 0 && index < 4); + return this->vec()[index]; + } + + /** Returns one component. Asserts if index is out of range and SK_DEBUG is defined. + + @param index one of: 0 (fR), 1 (fG), 2 (fB), 3 (fA) + @return value corresponding to index + */ + float& operator[](int index) { + SkASSERT(index >= 0 && index < 4); + return this->vec()[index]; + } + + /** Returns true if SkRGBA4f is an opaque color. Asserts if fA is out of range and + SK_DEBUG is defined. + + @return true if SkRGBA4f is opaque + */ + bool isOpaque() const { + SkASSERT(fA <= 1.0f && fA >= 0.0f); + return fA == 1.0f; + } + + /** Returns true if all channels are in [0, 1]. */ + bool fitsInBytes() const { + SkASSERT(fA >= 0.0f && fA <= 1.0f); + return fR >= 0.0f && fR <= 1.0f && + fG >= 0.0f && fG <= 1.0f && + fB >= 0.0f && fB <= 1.0f; + } + + /** Returns closest SkRGBA4f to SkColor. Only allowed if SkRGBA4f is unpremultiplied. + + @param color Color with Alpha, red, blue, and green components + @return SkColor as SkRGBA4f + + example: https://fiddle.skia.org/c/@RGBA4f_FromColor + */ + static SkRGBA4f FromColor(SkColor color); // impl. depends on kAT + + /** Returns closest SkColor to SkRGBA4f. Only allowed if SkRGBA4f is unpremultiplied. + + @return color as SkColor + + example: https://fiddle.skia.org/c/@RGBA4f_toSkColor + */ + SkColor toSkColor() const; // impl. depends on kAT + + /** Returns closest SkRGBA4f to SkPMColor. Only allowed if SkRGBA4f is premultiplied. + + @return SkPMColor as SkRGBA4f + */ + static SkRGBA4f FromPMColor(SkPMColor); // impl. depends on kAT + + /** Returns SkRGBA4f premultiplied by alpha. Asserts at compile time if SkRGBA4f is + already premultiplied. + + @return premultiplied color + */ + SkRGBA4f premul() const { + static_assert(kAT == kUnpremul_SkAlphaType, ""); + return { fR * fA, fG * fA, fB * fA, fA }; + } + + /** Returns SkRGBA4f unpremultiplied by alpha. Asserts at compile time if SkRGBA4f is + already unpremultiplied. + + @return unpremultiplied color + */ + SkRGBA4f unpremul() const { + static_assert(kAT == kPremul_SkAlphaType, ""); + + if (fA == 0.0f) { + return { 0, 0, 0, 0 }; + } else { + float invAlpha = 1 / fA; + return { fR * invAlpha, fG * invAlpha, fB * invAlpha, fA }; + } + } + + // This produces bytes in RGBA order (eg GrColor). Impl. is the same, regardless of kAT + uint32_t toBytes_RGBA() const; + static SkRGBA4f FromBytes_RGBA(uint32_t color); + + /** + Returns a copy of the SkRGBA4f but with alpha component set to 1.0f. + + @return opaque color + */ + SkRGBA4f makeOpaque() const { + return { fR, fG, fB, 1.0f }; + } +}; + +/** \struct SkColor4f + RGBA color value, holding four floating point components. Color components are always in + a known order, and are unpremultiplied. + + This is a specialization of SkRGBA4f. For details, @see SkRGBA4f. +*/ +using SkColor4f = SkRGBA4f; + +template <> SK_API SkColor4f SkColor4f::FromColor(SkColor); +template <> SK_API SkColor SkColor4f::toSkColor() const; +template <> SK_API uint32_t SkColor4f::toBytes_RGBA() const; +template <> SK_API SkColor4f SkColor4f::FromBytes_RGBA(uint32_t color); + +namespace SkColors { +constexpr SkColor4f kTransparent = {0, 0, 0, 0}; +constexpr SkColor4f kBlack = {0, 0, 0, 1}; +constexpr SkColor4f kDkGray = {0.25f, 0.25f, 0.25f, 1}; +constexpr SkColor4f kGray = {0.50f, 0.50f, 0.50f, 1}; +constexpr SkColor4f kLtGray = {0.75f, 0.75f, 0.75f, 1}; +constexpr SkColor4f kWhite = {1, 1, 1, 1}; +constexpr SkColor4f kRed = {1, 0, 0, 1}; +constexpr SkColor4f kGreen = {0, 1, 0, 1}; +constexpr SkColor4f kBlue = {0, 0, 1, 1}; +constexpr SkColor4f kYellow = {1, 1, 0, 1}; +constexpr SkColor4f kCyan = {0, 1, 1, 1}; +constexpr SkColor4f kMagenta = {1, 0, 1, 1}; +} // namespace SkColors +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorFilter.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorFilter.h new file mode 100644 index 00000000000..0898f0acadd --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorFilter.h @@ -0,0 +1,156 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkColorFilter_DEFINED +#define SkColorFilter_DEFINED + +#include "include/core/SkColor.h" +#include "include/core/SkFlattenable.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +#include +#include +#include + +class SkColorMatrix; +class SkColorSpace; +class SkColorTable; + +enum class SkBlendMode; +struct SkDeserialProcs; + +/** +* ColorFilters are optional objects in the drawing pipeline. When present in +* a paint, they are called with the "src" colors, and return new colors, which +* are then passed onto the next stage (either ImageFilter or Xfermode). +* +* All subclasses are required to be reentrant-safe : it must be legal to share +* the same instance between several threads. +*/ +class SK_API SkColorFilter : public SkFlattenable { +public: + /** If the filter can be represented by a source color plus Mode, this + * returns true, and sets (if not NULL) the color and mode appropriately. + * If not, this returns false and ignores the parameters. + */ + bool asAColorMode(SkColor* color, SkBlendMode* mode) const; + + /** If the filter can be represented by a 5x4 matrix, this + * returns true, and sets the matrix appropriately. + * If not, this returns false and ignores the parameter. + */ + bool asAColorMatrix(float matrix[20]) const; + + // Returns true if the filter is guaranteed to never change the alpha of a color it filters. + bool isAlphaUnchanged() const; + + /** + * Applies this filter to the input color. This function does no color management. + * + * DEPRECATED: Please use filterColor4f instead. That function supports higher precision, + * wide-gamut color, and is explicit about the color space of the input and output. + */ + SkColor filterColor(SkColor) const; + + /** + * Converts the src color (in src colorspace), into the dst colorspace, + * then applies this filter to it, returning the filtered color in the dst colorspace. + */ + SkColor4f filterColor4f(const SkColor4f& srcColor, SkColorSpace* srcCS, + SkColorSpace* dstCS) const; + + /** Construct a colorfilter whose effect is to first apply the inner filter and then apply + * this filter, applied to the output of the inner filter. + * + * result = this(inner(...)) + */ + sk_sp makeComposed(sk_sp inner) const; + + /** Return a colorfilter that will compute this filter in a specific color space. By default all + * filters operate in the destination (surface) color space. This allows filters like Blend and + * Matrix, or runtime color filters to perform their math in a known space. + */ + sk_sp makeWithWorkingColorSpace(sk_sp) const; + + static sk_sp Deserialize(const void* data, size_t size, + const SkDeserialProcs* procs = nullptr); + +private: + SkColorFilter() = default; + friend class SkColorFilterBase; + + using INHERITED = SkFlattenable; +}; + +class SK_API SkColorFilters { +public: + static sk_sp Compose(const sk_sp& outer, + sk_sp inner) { + return outer ? outer->makeComposed(std::move(inner)) + : std::move(inner); + } + + // Blends between the constant color (src) and input color (dst) based on the SkBlendMode. + // If the color space is null, the constant color is assumed to be defined in sRGB. + static sk_sp Blend(const SkColor4f& c, sk_sp, SkBlendMode mode); + static sk_sp Blend(SkColor c, SkBlendMode mode); + + static sk_sp Matrix(const SkColorMatrix&); + static sk_sp Matrix(const float rowMajor[20]); + + // A version of Matrix which operates in HSLA space instead of RGBA. + // I.e. HSLA-to-RGBA(Matrix(RGBA-to-HSLA(input))). + static sk_sp HSLAMatrix(const SkColorMatrix&); + static sk_sp HSLAMatrix(const float rowMajor[20]); + + static sk_sp LinearToSRGBGamma(); + static sk_sp SRGBToLinearGamma(); + static sk_sp Lerp(float t, sk_sp dst, sk_sp src); + + /** + * Create a table colorfilter, copying the table into the filter, and + * applying it to all 4 components. + * a' = table[a]; + * r' = table[r]; + * g' = table[g]; + * b' = table[b]; + * Components are operated on in unpremultiplied space. If the incomming + * colors are premultiplied, they are temporarily unpremultiplied, then + * the table is applied, and then the result is remultiplied. + */ + static sk_sp Table(const uint8_t table[256]); + + /** + * Create a table colorfilter, with a different table for each + * component [A, R, G, B]. If a given table is NULL, then it is + * treated as identity, with the component left unchanged. If a table + * is not null, then its contents are copied into the filter. + */ + static sk_sp TableARGB(const uint8_t tableA[256], + const uint8_t tableR[256], + const uint8_t tableG[256], + const uint8_t tableB[256]); + + /** + * Create a table colorfilter that holds a ref to the shared color table. + */ + static sk_sp Table(sk_sp table); + + /** + * Create a colorfilter that multiplies the RGB channels by one color, and + * then adds a second color, pinning the result for each component to + * [0..255]. The alpha components of the mul and add arguments + * are ignored. + */ + static sk_sp Lighting(SkColor mul, SkColor add); + +private: + SkColorFilters() = delete; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorPriv.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorPriv.h new file mode 100644 index 00000000000..f89de9db72f --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorPriv.h @@ -0,0 +1,167 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkColorPriv_DEFINED +#define SkColorPriv_DEFINED + +#include "include/core/SkColor.h" +#include "include/private/base/SkMath.h" +#include "include/private/base/SkTPin.h" +#include "include/private/base/SkTo.h" + +#include + +/** Turn 0..255 into 0..256 by adding 1 at the half-way point. Used to turn a + byte into a scale value, so that we can say scale * value >> 8 instead of + alpha * value / 255. + + In debugging, asserts that alpha is 0..255 +*/ +static inline unsigned SkAlpha255To256(U8CPU alpha) { + SkASSERT(SkToU8(alpha) == alpha); + // this one assues that blending on top of an opaque dst keeps it that way + // even though it is less accurate than a+(a>>7) for non-opaque dsts + return alpha + 1; +} + +/** Multiplify value by 0..256, and shift the result down 8 + (i.e. return (value * alpha256) >> 8) + */ +#define SkAlphaMul(value, alpha256) (((value) * (alpha256)) >> 8) + +static inline U8CPU SkUnitScalarClampToByte(SkScalar x) { + return static_cast(SkTPin(x, 0.0f, 1.0f) * 255 + 0.5); +} + +#define SK_A32_BITS 8 +#define SK_R32_BITS 8 +#define SK_G32_BITS 8 +#define SK_B32_BITS 8 + +#define SK_A32_MASK ((1 << SK_A32_BITS) - 1) +#define SK_R32_MASK ((1 << SK_R32_BITS) - 1) +#define SK_G32_MASK ((1 << SK_G32_BITS) - 1) +#define SK_B32_MASK ((1 << SK_B32_BITS) - 1) + +/* + * Skia's 32bit backend only supports 1 swizzle order at a time (compile-time). + * This is specified by SK_R32_SHIFT=0 or SK_R32_SHIFT=16. + * + * For easier compatibility with Skia's GPU backend, we further restrict these + * to either (in memory-byte-order) RGBA or BGRA. Note that this "order" does + * not directly correspond to the same shift-order, since we have to take endianess + * into account. + * + * Here we enforce this constraint. + */ + +#define SK_RGBA_R32_SHIFT 0 +#define SK_RGBA_G32_SHIFT 8 +#define SK_RGBA_B32_SHIFT 16 +#define SK_RGBA_A32_SHIFT 24 + +#define SK_BGRA_B32_SHIFT 0 +#define SK_BGRA_G32_SHIFT 8 +#define SK_BGRA_R32_SHIFT 16 +#define SK_BGRA_A32_SHIFT 24 + +#if defined(SK_PMCOLOR_IS_RGBA) || defined(SK_PMCOLOR_IS_BGRA) + #error "Configure PMCOLOR by setting SK_R32_SHIFT." +#endif + +// Deduce which SK_PMCOLOR_IS_ to define from the _SHIFT defines + +#if (SK_A32_SHIFT == SK_RGBA_A32_SHIFT && \ + SK_R32_SHIFT == SK_RGBA_R32_SHIFT && \ + SK_G32_SHIFT == SK_RGBA_G32_SHIFT && \ + SK_B32_SHIFT == SK_RGBA_B32_SHIFT) + #define SK_PMCOLOR_IS_RGBA +#elif (SK_A32_SHIFT == SK_BGRA_A32_SHIFT && \ + SK_R32_SHIFT == SK_BGRA_R32_SHIFT && \ + SK_G32_SHIFT == SK_BGRA_G32_SHIFT && \ + SK_B32_SHIFT == SK_BGRA_B32_SHIFT) + #define SK_PMCOLOR_IS_BGRA +#else + #error "need 32bit packing to be either RGBA or BGRA" +#endif + +#define SkGetPackedA32(packed) ((uint32_t)((packed) << (24 - SK_A32_SHIFT)) >> 24) +#define SkGetPackedR32(packed) ((uint32_t)((packed) << (24 - SK_R32_SHIFT)) >> 24) +#define SkGetPackedG32(packed) ((uint32_t)((packed) << (24 - SK_G32_SHIFT)) >> 24) +#define SkGetPackedB32(packed) ((uint32_t)((packed) << (24 - SK_B32_SHIFT)) >> 24) + +#define SkA32Assert(a) SkASSERT((unsigned)(a) <= SK_A32_MASK) +#define SkR32Assert(r) SkASSERT((unsigned)(r) <= SK_R32_MASK) +#define SkG32Assert(g) SkASSERT((unsigned)(g) <= SK_G32_MASK) +#define SkB32Assert(b) SkASSERT((unsigned)(b) <= SK_B32_MASK) + +/** + * Pack the components into a SkPMColor, checking (in the debug version) that + * the components are 0..255, and are already premultiplied (i.e. alpha >= color) + */ +static inline SkPMColor SkPackARGB32(U8CPU a, U8CPU r, U8CPU g, U8CPU b) { + SkA32Assert(a); + SkASSERT(r <= a); + SkASSERT(g <= a); + SkASSERT(b <= a); + + return (a << SK_A32_SHIFT) | (r << SK_R32_SHIFT) | + (g << SK_G32_SHIFT) | (b << SK_B32_SHIFT); +} + +/** + * Same as SkPackARGB32, but this version guarantees to not check that the + * values are premultiplied in the debug version. + */ +static inline SkPMColor SkPackARGB32NoCheck(U8CPU a, U8CPU r, U8CPU g, U8CPU b) { + return (a << SK_A32_SHIFT) | (r << SK_R32_SHIFT) | + (g << SK_G32_SHIFT) | (b << SK_B32_SHIFT); +} + +static inline +SkPMColor SkPremultiplyARGBInline(U8CPU a, U8CPU r, U8CPU g, U8CPU b) { + SkA32Assert(a); + SkR32Assert(r); + SkG32Assert(g); + SkB32Assert(b); + + if (a != 255) { + r = SkMulDiv255Round(r, a); + g = SkMulDiv255Round(g, a); + b = SkMulDiv255Round(b, a); + } + return SkPackARGB32(a, r, g, b); +} + +// When Android is compiled optimizing for size, SkAlphaMulQ doesn't get +// inlined; forcing inlining significantly improves performance. +static SK_ALWAYS_INLINE uint32_t SkAlphaMulQ(uint32_t c, unsigned scale) { + uint32_t mask = 0xFF00FF; + + uint32_t rb = ((c & mask) * scale) >> 8; + uint32_t ag = ((c >> 8) & mask) * scale; + return (rb & mask) | (ag & ~mask); +} + +static inline SkPMColor SkPMSrcOver(SkPMColor src, SkPMColor dst) { + uint32_t scale = SkAlpha255To256(255 - SkGetPackedA32(src)); + + uint32_t mask = 0xFF00FF; + uint32_t rb = (((dst & mask) * scale) >> 8) & mask; + uint32_t ag = (((dst >> 8) & mask) * scale) & ~mask; + + rb += (src & mask); + ag += (src & ~mask); + + // Color channels (but not alpha) can overflow, so we have to saturate to 0xFF in each lane. + return std::min(rb & 0x000001FF, 0x000000FFU) | + std::min(ag & 0x0001FF00, 0x0000FF00U) | + std::min(rb & 0x01FF0000, 0x00FF0000U) | + (ag & 0xFF000000); +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorSpace.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorSpace.h new file mode 100644 index 00000000000..57c29e222a4 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorSpace.h @@ -0,0 +1,242 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkColorSpace_DEFINED +#define SkColorSpace_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkFixed.h" +#include "include/private/base/SkOnce.h" +#include "modules/skcms/skcms.h" + +#include +#include + +class SkData; + +/** + * Describes a color gamut with primaries and a white point. + */ +struct SK_API SkColorSpacePrimaries { + float fRX; + float fRY; + float fGX; + float fGY; + float fBX; + float fBY; + float fWX; + float fWY; + + /** + * Convert primaries and a white point to a toXYZD50 matrix, the preferred color gamut + * representation of SkColorSpace. + */ + bool toXYZD50(skcms_Matrix3x3* toXYZD50) const; +}; + +namespace SkNamedTransferFn { + +// Like SkNamedGamut::kSRGB, keeping this bitwise exactly the same as skcms makes things fastest. +static constexpr skcms_TransferFunction kSRGB = + { 2.4f, (float)(1/1.055), (float)(0.055/1.055), (float)(1/12.92), 0.04045f, 0.0f, 0.0f }; + +static constexpr skcms_TransferFunction k2Dot2 = + { 2.2f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; + +static constexpr skcms_TransferFunction kLinear = + { 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; + +static constexpr skcms_TransferFunction kRec2020 = + {2.22222f, 0.909672f, 0.0903276f, 0.222222f, 0.0812429f, 0, 0}; + +static constexpr skcms_TransferFunction kPQ = + {-2.0f, -107/128.0f, 1.0f, 32/2523.0f, 2413/128.0f, -2392/128.0f, 8192/1305.0f }; + +static constexpr skcms_TransferFunction kHLG = + {-3.0f, 2.0f, 2.0f, 1/0.17883277f, 0.28466892f, 0.55991073f, 0.0f }; + +} // namespace SkNamedTransferFn + +namespace SkNamedGamut { + +static constexpr skcms_Matrix3x3 kSRGB = {{ + // ICC fixed-point (16.16) representation, taken from skcms. Please keep them exactly in sync. + // 0.436065674f, 0.385147095f, 0.143066406f, + // 0.222488403f, 0.716873169f, 0.060607910f, + // 0.013916016f, 0.097076416f, 0.714096069f, + { SkFixedToFloat(0x6FA2), SkFixedToFloat(0x6299), SkFixedToFloat(0x24A0) }, + { SkFixedToFloat(0x38F5), SkFixedToFloat(0xB785), SkFixedToFloat(0x0F84) }, + { SkFixedToFloat(0x0390), SkFixedToFloat(0x18DA), SkFixedToFloat(0xB6CF) }, +}}; + +static constexpr skcms_Matrix3x3 kAdobeRGB = {{ + // ICC fixed-point (16.16) repesentation of: + // 0.60974, 0.20528, 0.14919, + // 0.31111, 0.62567, 0.06322, + // 0.01947, 0.06087, 0.74457, + { SkFixedToFloat(0x9c18), SkFixedToFloat(0x348d), SkFixedToFloat(0x2631) }, + { SkFixedToFloat(0x4fa5), SkFixedToFloat(0xa02c), SkFixedToFloat(0x102f) }, + { SkFixedToFloat(0x04fc), SkFixedToFloat(0x0f95), SkFixedToFloat(0xbe9c) }, +}}; + +static constexpr skcms_Matrix3x3 kDisplayP3 = {{ + { 0.515102f, 0.291965f, 0.157153f }, + { 0.241182f, 0.692236f, 0.0665819f }, + { -0.00104941f, 0.0418818f, 0.784378f }, +}}; + +static constexpr skcms_Matrix3x3 kRec2020 = {{ + { 0.673459f, 0.165661f, 0.125100f }, + { 0.279033f, 0.675338f, 0.0456288f }, + { -0.00193139f, 0.0299794f, 0.797162f }, +}}; + +static constexpr skcms_Matrix3x3 kXYZ = {{ + { 1.0f, 0.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f }, +}}; + +} // namespace SkNamedGamut + +class SK_API SkColorSpace : public SkNVRefCnt { +public: + /** + * Create the sRGB color space. + */ + static sk_sp MakeSRGB(); + + /** + * Colorspace with the sRGB primaries, but a linear (1.0) gamma. + */ + static sk_sp MakeSRGBLinear(); + + /** + * Create an SkColorSpace from a transfer function and a row-major 3x3 transformation to XYZ. + */ + static sk_sp MakeRGB(const skcms_TransferFunction& transferFn, + const skcms_Matrix3x3& toXYZ); + + /** + * Create an SkColorSpace from a parsed (skcms) ICC profile. + */ + static sk_sp Make(const skcms_ICCProfile&); + + /** + * Convert this color space to an skcms ICC profile struct. + */ + void toProfile(skcms_ICCProfile*) const; + + /** + * Returns true if the color space gamma is near enough to be approximated as sRGB. + */ + bool gammaCloseToSRGB() const; + + /** + * Returns true if the color space gamma is linear. + */ + bool gammaIsLinear() const; + + /** + * Sets |fn| to the transfer function from this color space. Returns true if the transfer + * function can be represented as coefficients to the standard ICC 7-parameter equation. + * Returns false otherwise (eg, PQ, HLG). + */ + bool isNumericalTransferFn(skcms_TransferFunction* fn) const; + + /** + * Returns true and sets |toXYZD50|. + */ + bool toXYZD50(skcms_Matrix3x3* toXYZD50) const; + + /** + * Returns a hash of the gamut transformation to XYZ D50. Allows for fast equality checking + * of gamuts, at the (very small) risk of collision. + */ + uint32_t toXYZD50Hash() const { return fToXYZD50Hash; } + + /** + * Returns a color space with the same gamut as this one, but with a linear gamma. + */ + sk_sp makeLinearGamma() const; + + /** + * Returns a color space with the same gamut as this one, but with the sRGB transfer + * function. + */ + sk_sp makeSRGBGamma() const; + + /** + * Returns a color space with the same transfer function as this one, but with the primary + * colors rotated. In other words, this produces a new color space that maps RGB to GBR + * (when applied to a source), and maps RGB to BRG (when applied to a destination). + * + * This is used for testing, to construct color spaces that have severe and testable behavior. + */ + sk_sp makeColorSpin() const; + + /** + * Returns true if the color space is sRGB. + * Returns false otherwise. + * + * This allows a little bit of tolerance, given that we might see small numerical error + * in some cases: converting ICC fixed point to float, converting white point to D50, + * rounding decisions on transfer function and matrix. + * + * This does not consider a 2.2f exponential transfer function to be sRGB. While these + * functions are similar (and it is sometimes useful to consider them together), this + * function checks for logical equality. + */ + bool isSRGB() const; + + /** + * Returns a serialized representation of this color space. + */ + sk_sp serialize() const; + + /** + * If |memory| is nullptr, returns the size required to serialize. + * Otherwise, serializes into |memory| and returns the size. + */ + size_t writeToMemory(void* memory) const; + + static sk_sp Deserialize(const void* data, size_t length); + + /** + * If both are null, we return true. If one is null and the other is not, we return false. + * If both are non-null, we do a deeper compare. + */ + static bool Equals(const SkColorSpace*, const SkColorSpace*); + + void transferFn(float gabcdef[7]) const; // DEPRECATED: Remove when webview usage is gone + void transferFn(skcms_TransferFunction* fn) const; + void invTransferFn(skcms_TransferFunction* fn) const; + void gamutTransformTo(const SkColorSpace* dst, skcms_Matrix3x3* src_to_dst) const; + + uint32_t transferFnHash() const { return fTransferFnHash; } + uint64_t hash() const { return (uint64_t)fTransferFnHash << 32 | fToXYZD50Hash; } + +private: + friend class SkColorSpaceSingletonFactory; + + SkColorSpace(const skcms_TransferFunction& transferFn, const skcms_Matrix3x3& toXYZ); + + void computeLazyDstFields() const; + + uint32_t fTransferFnHash; + uint32_t fToXYZD50Hash; + + skcms_TransferFunction fTransferFn; + skcms_Matrix3x3 fToXYZD50; + + mutable skcms_TransferFunction fInvTransferFn; + mutable skcms_Matrix3x3 fFromXYZD50; + mutable SkOnce fLazyDstFieldsOnce; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorTable.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorTable.h new file mode 100644 index 00000000000..dc16048a598 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorTable.h @@ -0,0 +1,62 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkColorTable_DEFINED +#define SkColorTable_DEFINED + +#include "include/core/SkBitmap.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +#include + +class SkReadBuffer; +class SkWriteBuffer; + +/** + * SkColorTable holds the lookup tables for each channel (ARGB) used to define the filter behavior + * of `SkColorFilters::Table`, and provides a way to share the table data between client code and + * the returned SkColorFilter. Once created, an SkColorTable is immutable. +*/ +class SK_API SkColorTable : public SkRefCnt { +public: + // Creates a new SkColorTable with 'table' used for all four channels. The table is copied into + // the SkColorTable. + static sk_sp Make(const uint8_t table[256]) { + return Make(table, table, table, table); + } + + // Creates a new SkColorTable with the per-channel lookup tables. Each non-null table is copied + // into the SkColorTable. Null parameters are interpreted as the identity table. + static sk_sp Make(const uint8_t tableA[256], + const uint8_t tableR[256], + const uint8_t tableG[256], + const uint8_t tableB[256]); + + // Per-channel constant value lookup (0-255). + const uint8_t* alphaTable() const { return fTable.getAddr8(0, 0); } + const uint8_t* redTable() const { return fTable.getAddr8(0, 1); } + const uint8_t* greenTable() const { return fTable.getAddr8(0, 2); } + const uint8_t* blueTable() const { return fTable.getAddr8(0, 3); } + + void flatten(SkWriteBuffer& buffer) const; + + static sk_sp Deserialize(SkReadBuffer& buffer); + +private: + friend class SkTableColorFilter; // for bitmap() + + SkColorTable(const SkBitmap& table) : fTable(table) {} + + // The returned SkBitmap is immutable; attempting to modify its pixel data will trigger asserts + // in debug builds and cause undefined behavior in release builds. + const SkBitmap& bitmap() const { return fTable; } + + SkBitmap fTable; // A 256x4 A8 image +}; + +#endif // SkColorTable_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorType.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorType.h new file mode 100644 index 00000000000..2b2837a0405 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkColorType.h @@ -0,0 +1,70 @@ +/* + * Copyright 2022 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkColorType_DEFINED +#define SkColorType_DEFINED + +#include "include/core/SkTypes.h" + +/** \enum SkColorType + Describes how pixel bits encode color. A pixel may be an alpha mask, a grayscale, RGB, or ARGB. + + kN32_SkColorType selects the native 32-bit ARGB format for the current configuration. This can + lead to inconsistent results across platforms, so use with caution. +*/ +enum SkColorType : int { + kUnknown_SkColorType, //!< uninitialized + kAlpha_8_SkColorType, //!< pixel with alpha in 8-bit byte + kRGB_565_SkColorType, //!< pixel with 5 bits red, 6 bits green, 5 bits blue, in 16-bit word + kARGB_4444_SkColorType, //!< pixel with 4 bits for alpha, red, green, blue; in 16-bit word + kRGBA_8888_SkColorType, //!< pixel with 8 bits for red, green, blue, alpha; in 32-bit word + kRGB_888x_SkColorType, //!< pixel with 8 bits each for red, green, blue; in 32-bit word + kBGRA_8888_SkColorType, //!< pixel with 8 bits for blue, green, red, alpha; in 32-bit word + kRGBA_1010102_SkColorType, //!< 10 bits for red, green, blue; 2 bits for alpha; in 32-bit word + kBGRA_1010102_SkColorType, //!< 10 bits for blue, green, red; 2 bits for alpha; in 32-bit word + kRGB_101010x_SkColorType, //!< pixel with 10 bits each for red, green, blue; in 32-bit word + kBGR_101010x_SkColorType, //!< pixel with 10 bits each for blue, green, red; in 32-bit word + kBGR_101010x_XR_SkColorType, //!< pixel with 10 bits each for blue, green, red; in 32-bit word, extended range + kBGRA_10101010_XR_SkColorType, //!< pixel with 10 bits each for blue, green, red, alpha; in 64-bit word, extended range + kRGBA_10x6_SkColorType, //!< pixel with 10 used bits (most significant) followed by 6 unused + // bits for red, green, blue, alpha; in 64-bit word + kGray_8_SkColorType, //!< pixel with grayscale level in 8-bit byte + kRGBA_F16Norm_SkColorType, //!< pixel with half floats in [0,1] for red, green, blue, alpha; + // in 64-bit word + kRGBA_F16_SkColorType, //!< pixel with half floats for red, green, blue, alpha; + // in 64-bit word + kRGBA_F32_SkColorType, //!< pixel using C float for red, green, blue, alpha; in 128-bit word + + // The following 6 colortypes are just for reading from - not for rendering to + kR8G8_unorm_SkColorType, //!< pixel with a uint8_t for red and green + + kA16_float_SkColorType, //!< pixel with a half float for alpha + kR16G16_float_SkColorType, //!< pixel with a half float for red and green + + kA16_unorm_SkColorType, //!< pixel with a little endian uint16_t for alpha + kR16G16_unorm_SkColorType, //!< pixel with a little endian uint16_t for red and green + kR16G16B16A16_unorm_SkColorType, //!< pixel with a little endian uint16_t for red, green, blue + // and alpha + + kSRGBA_8888_SkColorType, + kR8_unorm_SkColorType, + + kLastEnum_SkColorType = kR8_unorm_SkColorType, //!< last valid value + +#if SK_PMCOLOR_BYTE_ORDER(B,G,R,A) + kN32_SkColorType = kBGRA_8888_SkColorType,//!< native 32-bit BGRA encoding + +#elif SK_PMCOLOR_BYTE_ORDER(R,G,B,A) + kN32_SkColorType = kRGBA_8888_SkColorType,//!< native 32-bit RGBA encoding + +#else + #error "SK_*32_SHIFT values must correspond to BGRA or RGBA byte order" +#endif +}; +static constexpr int kSkColorTypeCnt = static_cast(kLastEnum_SkColorType) + 1; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkContourMeasure.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkContourMeasure.h new file mode 100644 index 00000000000..29e33d84f30 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkContourMeasure.h @@ -0,0 +1,139 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkContourMeasure_DEFINED +#define SkContourMeasure_DEFINED + +#include "include/core/SkPoint.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkTDArray.h" + +#include + +class SkMatrix; +class SkPath; + +class SK_API SkContourMeasure : public SkRefCnt { +public: + /** Return the length of the contour. + */ + SkScalar length() const { return fLength; } + + /** Pins distance to 0 <= distance <= length(), and then computes the corresponding + * position and tangent. + */ + [[nodiscard]] bool getPosTan(SkScalar distance, SkPoint* position, SkVector* tangent) const; + + enum MatrixFlags { + kGetPosition_MatrixFlag = 0x01, + kGetTangent_MatrixFlag = 0x02, + kGetPosAndTan_MatrixFlag = kGetPosition_MatrixFlag | kGetTangent_MatrixFlag + }; + + /** Pins distance to 0 <= distance <= getLength(), and then computes + the corresponding matrix (by calling getPosTan). + Returns false if there is no path, or a zero-length path was specified, in which case + matrix is unchanged. + */ + [[nodiscard]] bool getMatrix(SkScalar distance, SkMatrix* matrix, + MatrixFlags flags = kGetPosAndTan_MatrixFlag) const; + + /** Given a start and stop distance, return in dst the intervening segment(s). + If the segment is zero-length, return false, else return true. + startD and stopD are pinned to legal values (0..getLength()). If startD > stopD + then return false (and leave dst untouched). + Begin the segment with a moveTo if startWithMoveTo is true + */ + [[nodiscard]] bool getSegment(SkScalar startD, SkScalar stopD, SkPath* dst, + bool startWithMoveTo) const; + + /** Return true if the contour is closed() + */ + bool isClosed() const { return fIsClosed; } + +private: + struct Segment { + SkScalar fDistance; // total distance up to this point + unsigned fPtIndex; // index into the fPts array + unsigned fTValue : 30; + unsigned fType : 2; // actually the enum SkSegType + // See SkPathMeasurePriv.h + + SkScalar getScalarT() const; + + static const Segment* Next(const Segment* seg) { + unsigned ptIndex = seg->fPtIndex; + do { + ++seg; + } while (seg->fPtIndex == ptIndex); + return seg; + } + + }; + + const SkTDArray fSegments; + const SkTDArray fPts; // Points used to define the segments + + const SkScalar fLength; + const bool fIsClosed; + + SkContourMeasure(SkTDArray&& segs, SkTDArray&& pts, + SkScalar length, bool isClosed); + ~SkContourMeasure() override {} + + const Segment* distanceToSegment(SkScalar distance, SkScalar* t) const; + + friend class SkContourMeasureIter; + friend class SkPathMeasurePriv; +}; + +class SK_API SkContourMeasureIter { +public: + SkContourMeasureIter(); + /** + * Initialize the Iter with a path. + * The parts of the path that are needed are copied, so the client is free to modify/delete + * the path after this call. + * + * resScale controls the precision of the measure. values > 1 increase the + * precision (and possibly slow down the computation). + */ + SkContourMeasureIter(const SkPath& path, bool forceClosed, SkScalar resScale = 1); + ~SkContourMeasureIter(); + + SkContourMeasureIter(SkContourMeasureIter&&); + SkContourMeasureIter& operator=(SkContourMeasureIter&&); + + /** + * Reset the Iter with a path. + * The parts of the path that are needed are copied, so the client is free to modify/delete + * the path after this call. + */ + void reset(const SkPath& path, bool forceClosed, SkScalar resScale = 1); + + /** + * Iterates through contours in path, returning a contour-measure object for each contour + * in the path. Returns null when it is done. + * + * This only returns non-zero length contours, where a contour is the segments between + * a kMove_Verb and either ... + * - the next kMove_Verb + * - kClose_Verb (1 or more) + * - kDone_Verb + * If it encounters a zero-length contour, it is skipped. + */ + sk_sp next(); + +private: + class Impl; + + std::unique_ptr fImpl; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCoverageMode.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCoverageMode.h new file mode 100644 index 00000000000..aaae60c4192 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCoverageMode.h @@ -0,0 +1,28 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkCoverageMode_DEFINED +#define SkCoverageMode_DEFINED + +/** + * Describes geometric operations (ala SkRegion::Op) that can be applied to coverage bytes. + * These can be thought of as variants of porter-duff (SkBlendMode) modes, but only applied + * to the alpha channel. + * + * See SkMaskFilter for ways to use these when combining two different masks. + */ +enum class SkCoverageMode { + kUnion, // A ∪ B A+B-A*B + kIntersect, // A ∩ B A*B + kDifference, // A - B A*(1-B) + kReverseDifference, // B - A B*(1-A) + kXor, // A ⊕ B A+B-2*A*B + + kLast = kXor, +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCubicMap.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCubicMap.h new file mode 100644 index 00000000000..863c9333f6e --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkCubicMap.h @@ -0,0 +1,47 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkCubicMap_DEFINED +#define SkCubicMap_DEFINED + +#include "include/core/SkPoint.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" + +/** + * Fast evaluation of a cubic ease-in / ease-out curve. This is defined as a parametric cubic + * curve inside the unit square. + * + * pt[0] is implicitly { 0, 0 } + * pt[3] is implicitly { 1, 1 } + * pts[1,2].X are inside the unit [0..1] + */ +class SK_API SkCubicMap { +public: + SkCubicMap(SkPoint p1, SkPoint p2); + + static bool IsLinear(SkPoint p1, SkPoint p2) { + return SkScalarNearlyEqual(p1.fX, p1.fY) && SkScalarNearlyEqual(p2.fX, p2.fY); + } + + float computeYFromX(float x) const; + + SkPoint computeFromT(float t) const; + +private: + enum Type { + kLine_Type, // x == y + kCubeRoot_Type, // At^3 == x + kSolver_Type, // general monotonic cubic solver + }; + + SkPoint fCoeff[3]; + Type fType; +}; + +#endif + diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkData.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkData.h new file mode 100644 index 00000000000..2b50cebc81b --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkData.h @@ -0,0 +1,191 @@ +/* + * Copyright 2011 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkData_DEFINED +#define SkData_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkAssert.h" + +#include +#include + +class SkStream; + +/** + * SkData holds an immutable data buffer. Not only is the data immutable, + * but the actual ptr that is returned (by data() or bytes()) is guaranteed + * to always be the same for the life of this instance. + */ +class SK_API SkData final : public SkNVRefCnt { +public: + /** + * Returns the number of bytes stored. + */ + size_t size() const { return fSize; } + + bool isEmpty() const { return 0 == fSize; } + + /** + * Returns the ptr to the data. + */ + const void* data() const { return fPtr; } + + /** + * Like data(), returns a read-only ptr into the data, but in this case + * it is cast to uint8_t*, to make it easy to add an offset to it. + */ + const uint8_t* bytes() const { + return reinterpret_cast(fPtr); + } + + /** + * USE WITH CAUTION. + * This call will assert that the refcnt is 1, as a precaution against modifying the + * contents when another client/thread has access to the data. + */ + void* writable_data() { + if (fSize) { + // only assert we're unique if we're not empty + SkASSERT(this->unique()); + } + return const_cast(fPtr); + } + + /** + * Helper to copy a range of the data into a caller-provided buffer. + * Returns the actual number of bytes copied, after clamping offset and + * length to the size of the data. If buffer is NULL, it is ignored, and + * only the computed number of bytes is returned. + */ + size_t copyRange(size_t offset, size_t length, void* buffer) const; + + /** + * Returns true if these two objects have the same length and contents, + * effectively returning 0 == memcmp(...) + */ + bool equals(const SkData* other) const; + + /** + * Function that, if provided, will be called when the SkData goes out + * of scope, allowing for custom allocation/freeing of the data's contents. + */ + typedef void (*ReleaseProc)(const void* ptr, void* context); + + /** + * Create a new dataref by copying the specified data + */ + static sk_sp MakeWithCopy(const void* data, size_t length); + + + /** + * Create a new data with uninitialized contents. The caller should call writable_data() + * to write into the buffer, but this must be done before another ref() is made. + */ + static sk_sp MakeUninitialized(size_t length); + + /** + * Create a new data with zero-initialized contents. The caller should call writable_data() + * to write into the buffer, but this must be done before another ref() is made. + */ + static sk_sp MakeZeroInitialized(size_t length); + + /** + * Create a new dataref by copying the specified c-string + * (a null-terminated array of bytes). The returned SkData will have size() + * equal to strlen(cstr) + 1. If cstr is NULL, it will be treated the same + * as "". + */ + static sk_sp MakeWithCString(const char cstr[]); + + /** + * Create a new dataref, taking the ptr as is, and using the + * releaseproc to free it. The proc may be NULL. + */ + static sk_sp MakeWithProc(const void* ptr, size_t length, ReleaseProc proc, void* ctx); + + /** + * Call this when the data parameter is already const and will outlive the lifetime of the + * SkData. Suitable for with const globals. + */ + static sk_sp MakeWithoutCopy(const void* data, size_t length) { + return MakeWithProc(data, length, NoopReleaseProc, nullptr); + } + + /** + * Create a new dataref from a pointer allocated by malloc. The Data object + * takes ownership of that allocation, and will handling calling sk_free. + */ + static sk_sp MakeFromMalloc(const void* data, size_t length); + + /** + * Create a new dataref the file with the specified path. + * If the file cannot be opened, this returns NULL. + */ + static sk_sp MakeFromFileName(const char path[]); + + /** + * Create a new dataref from a stdio FILE. + * This does not take ownership of the FILE, nor close it. + * The caller is free to close the FILE at its convenience. + * The FILE must be open for reading only. + * Returns NULL on failure. + */ + static sk_sp MakeFromFILE(FILE* f); + + /** + * Create a new dataref from a file descriptor. + * This does not take ownership of the file descriptor, nor close it. + * The caller is free to close the file descriptor at its convenience. + * The file descriptor must be open for reading only. + * Returns NULL on failure. + */ + static sk_sp MakeFromFD(int fd); + + /** + * Attempt to read size bytes into a SkData. If the read succeeds, return the data, + * else return NULL. Either way the stream's cursor may have been changed as a result + * of calling read(). + */ + static sk_sp MakeFromStream(SkStream*, size_t size); + + /** + * Create a new dataref using a subset of the data in the specified + * src dataref. + */ + static sk_sp MakeSubset(const SkData* src, size_t offset, size_t length); + + /** + * Returns a new empty dataref (or a reference to a shared empty dataref). + * New or shared, the caller must see that unref() is eventually called. + */ + static sk_sp MakeEmpty(); + +private: + friend class SkNVRefCnt; + ReleaseProc fReleaseProc; + void* fReleaseProcContext; + const void* fPtr; + size_t fSize; + + SkData(const void* ptr, size_t size, ReleaseProc, void* context); + explicit SkData(size_t size); // inplace new/delete + ~SkData(); + + // Ensure the unsized delete is called. + void operator delete(void* p); + + // shared internal factory + static sk_sp PrivateNewWithCopy(const void* srcOrNull, size_t length); + + static void NoopReleaseProc(const void*, void*); // {} + + using INHERITED = SkRefCnt; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkDataTable.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkDataTable.h new file mode 100644 index 00000000000..3aa48d5f33e --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkDataTable.h @@ -0,0 +1,122 @@ +/* + * Copyright 2013 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkDataTable_DEFINED +#define SkDataTable_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkAssert.h" + +#include +#include + +/** + * Like SkData, SkDataTable holds an immutable data buffer. The data buffer is + * organized into a table of entries, each with a length, so the entries are + * not required to all be the same size. + */ +class SK_API SkDataTable : public SkRefCnt { +public: + /** + * Returns true if the table is empty (i.e. has no entries). + */ + bool isEmpty() const { return 0 == fCount; } + + /** + * Return the number of entries in the table. 0 for an empty table + */ + int count() const { return fCount; } + + /** + * Return the size of the index'th entry in the table. The caller must + * ensure that index is valid for this table. + */ + size_t atSize(int index) const; + + /** + * Return a pointer to the data of the index'th entry in the table. + * The caller must ensure that index is valid for this table. + * + * @param size If non-null, this returns the byte size of this entry. This + * will be the same value that atSize(index) would return. + */ + const void* at(int index, size_t* size = nullptr) const; + + template + const T* atT(int index, size_t* size = nullptr) const { + return reinterpret_cast(this->at(index, size)); + } + + /** + * Returns the index'th entry as a c-string, and assumes that the trailing + * null byte had been copied into the table as well. + */ + const char* atStr(int index) const { + size_t size; + const char* str = this->atT(index, &size); + SkASSERT(strlen(str) + 1 == size); + return str; + } + + typedef void (*FreeProc)(void* context); + + static sk_sp MakeEmpty(); + + /** + * Return a new DataTable that contains a copy of the data stored in each + * "array". + * + * @param ptrs array of points to each element to be copied into the table. + * @param sizes array of byte-lengths for each entry in the corresponding + * ptrs[] array. + * @param count the number of array elements in ptrs[] and sizes[] to copy. + */ + static sk_sp MakeCopyArrays(const void * const * ptrs, + const size_t sizes[], int count); + + /** + * Return a new table that contains a copy of the data in array. + * + * @param array contiguous array of data for all elements to be copied. + * @param elemSize byte-length for a given element. + * @param count the number of entries to be copied out of array. The number + * of bytes that will be copied is count * elemSize. + */ + static sk_sp MakeCopyArray(const void* array, size_t elemSize, int count); + + static sk_sp MakeArrayProc(const void* array, size_t elemSize, int count, + FreeProc proc, void* context); + +private: + struct Dir { + const void* fPtr; + uintptr_t fSize; + }; + + int fCount; + size_t fElemSize; + union { + const Dir* fDir; + const char* fElems; + } fU; + + FreeProc fFreeProc; + void* fFreeProcContext; + + SkDataTable(); + SkDataTable(const void* array, size_t elemSize, int count, + FreeProc, void* context); + SkDataTable(const Dir*, int count, FreeProc, void* context); + ~SkDataTable() override; + + friend class SkDataTableBuilder; // access to Dir + + using INHERITED = SkRefCnt; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkDocument.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkDocument.h new file mode 100644 index 00000000000..c5fe5e850df --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkDocument.h @@ -0,0 +1,93 @@ +/* + * Copyright 2013 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkDocument_DEFINED +#define SkDocument_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/private/base/SkAPI.h" + +class SkCanvas; +class SkWStream; +struct SkRect; + +/** SK_ScalarDefaultDPI is 72 dots per inch. */ +static constexpr SkScalar SK_ScalarDefaultRasterDPI = 72.0f; + +/** + * High-level API for creating a document-based canvas. To use.. + * + * 1. Create a document, specifying a stream to store the output. + * 2. For each "page" of content: + * a. canvas = doc->beginPage(...) + * b. draw_my_content(canvas); + * c. doc->endPage(); + * 3. Close the document with doc->close(). + */ +class SK_API SkDocument : public SkRefCnt { +public: + + /** + * Begin a new page for the document, returning the canvas that will draw + * into the page. The document owns this canvas, and it will go out of + * scope when endPage() or close() is called, or the document is deleted. + * This will call endPage() if there is a currently active page. + */ + SkCanvas* beginPage(SkScalar width, SkScalar height, const SkRect* content = nullptr); + + /** + * Call endPage() when the content for the current page has been drawn + * (into the canvas returned by beginPage()). After this call the canvas + * returned by beginPage() will be out-of-scope. + */ + void endPage(); + + /** + * Call close() when all pages have been drawn. This will close the file + * or stream holding the document's contents. After close() the document + * can no longer add new pages. Deleting the document will automatically + * call close() if need be. + */ + void close(); + + /** + * Call abort() to stop producing the document immediately. + * The stream output must be ignored, and should not be trusted. + */ + void abort(); + +protected: + SkDocument(SkWStream*); + + // note: subclasses must call close() in their destructor, as the base class + // cannot do this for them. + ~SkDocument() override; + + virtual SkCanvas* onBeginPage(SkScalar width, SkScalar height) = 0; + virtual void onEndPage() = 0; + virtual void onClose(SkWStream*) = 0; + virtual void onAbort() = 0; + + // Allows subclasses to write to the stream as pages are written. + SkWStream* getStream() { return fStream; } + + enum State { + kBetweenPages_State, + kInPage_State, + kClosed_State + }; + State getState() const { return fState; } + +private: + SkWStream* fStream; + State fState; + + using INHERITED = SkRefCnt; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkDrawable.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkDrawable.h new file mode 100644 index 00000000000..764a8254498 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkDrawable.h @@ -0,0 +1,178 @@ +/* + * Copyright 2014 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkDrawable_DEFINED +#define SkDrawable_DEFINED + +#include "include/core/SkFlattenable.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/private/base/SkAPI.h" + +#include +#include +#include + +class GrBackendDrawableInfo; +class SkCanvas; +class SkMatrix; +class SkPicture; +enum class GrBackendApi : unsigned int; +struct SkDeserialProcs; +struct SkIRect; +struct SkImageInfo; +struct SkRect; + +/** + * Base-class for objects that draw into SkCanvas. + * + * The object has a generation ID, which is guaranteed to be unique across all drawables. To + * allow for clients of the drawable that may want to cache the results, the drawable must + * change its generation ID whenever its internal state changes such that it will draw differently. + */ +class SK_API SkDrawable : public SkFlattenable { +public: + /** + * Draws into the specified content. The drawing sequence will be balanced upon return + * (i.e. the saveLevel() on the canvas will match what it was when draw() was called, + * and the current matrix and clip settings will not be changed. + */ + void draw(SkCanvas*, const SkMatrix* = nullptr); + void draw(SkCanvas*, SkScalar x, SkScalar y); + + /** + * When using the GPU backend it is possible for a drawable to execute using the underlying 3D + * API rather than the SkCanvas API. It does so by creating a GpuDrawHandler. The GPU backend + * is deferred so the handler will be given access to the 3D API at the correct point in the + * drawing stream as the GPU backend flushes. Since the drawable may mutate, each time it is + * drawn to a GPU-backed canvas a new handler is snapped, representing the drawable's state at + * the time of the snap. + * + * When the GPU backend flushes to the 3D API it will call the draw method on the + * GpuDrawHandler. At this time the drawable may add commands to the stream of GPU commands for + * the unerlying 3D API. The draw function takes a GrBackendDrawableInfo which contains + * information about the current state of 3D API which the caller must respect. See + * GrBackendDrawableInfo for more specific details on what information is sent and the + * requirements for different 3D APIs. + * + * Additionaly there may be a slight delay from when the drawable adds its commands to when + * those commands are actually submitted to the GPU. Thus the drawable or GpuDrawHandler is + * required to keep any resources that are used by its added commands alive and valid until + * those commands are submitted to the GPU. The GpuDrawHandler will be kept alive and then + * deleted once the commands are submitted to the GPU. The dtor of the GpuDrawHandler is the + * signal to the drawable that the commands have all been submitted. Different 3D APIs may have + * additional requirements for certain resources which require waiting for the GPU to finish + * all work on those resources before reusing or deleting them. In this case, the drawable can + * use the dtor call of the GpuDrawHandler to add a fence to the GPU to track when the GPU work + * has completed. + * + * Currently this is only supported for the GPU Vulkan backend. + */ + + class GpuDrawHandler { + public: + virtual ~GpuDrawHandler() {} + + virtual void draw(const GrBackendDrawableInfo&) {} + }; + + /** + * Snaps off a GpuDrawHandler to represent the state of the SkDrawable at the time the snap is + * called. This is used for executing GPU backend specific draws intermixed with normal Skia GPU + * draws. The GPU API, which will be used for the draw, as well as the full matrix, device clip + * bounds and imageInfo of the target buffer are passed in as inputs. + */ + std::unique_ptr snapGpuDrawHandler(GrBackendApi backendApi, + const SkMatrix& matrix, + const SkIRect& clipBounds, + const SkImageInfo& bufferInfo) { + return this->onSnapGpuDrawHandler(backendApi, matrix, clipBounds, bufferInfo); + } + + /** + * Returns an SkPicture with the contents of this SkDrawable. + */ + sk_sp makePictureSnapshot(); + + /** + * Return a unique value for this instance. If two calls to this return the same value, + * it is presumed that calling the draw() method will render the same thing as well. + * + * Subclasses that change their state should call notifyDrawingChanged() to ensure that + * a new value will be returned the next time it is called. + */ + uint32_t getGenerationID(); + + /** + * Return the (conservative) bounds of what the drawable will draw. If the drawable can + * change what it draws (e.g. animation or in response to some external change), then this + * must return a bounds that is always valid for all possible states. + */ + SkRect getBounds(); + + /** + * Return approximately how many bytes would be freed if this drawable is destroyed. + * The base implementation returns 0 to indicate that this is unknown. + */ + size_t approximateBytesUsed(); + + /** + * Calling this invalidates the previous generation ID, and causes a new one to be computed + * the next time getGenerationID() is called. Typically this is called by the object itself, + * in response to its internal state changing. + */ + void notifyDrawingChanged(); + + static SkFlattenable::Type GetFlattenableType() { + return kSkDrawable_Type; + } + + SkFlattenable::Type getFlattenableType() const override { + return kSkDrawable_Type; + } + + static sk_sp Deserialize(const void* data, size_t size, + const SkDeserialProcs* procs = nullptr) { + return sk_sp(static_cast( + SkFlattenable::Deserialize( + kSkDrawable_Type, data, size, procs).release())); + } + + Factory getFactory() const override { return nullptr; } + const char* getTypeName() const override { return nullptr; } + +protected: + SkDrawable(); + + virtual SkRect onGetBounds() = 0; + virtual size_t onApproximateBytesUsed(); + virtual void onDraw(SkCanvas*) = 0; + + virtual std::unique_ptr onSnapGpuDrawHandler(GrBackendApi, const SkMatrix&, + const SkIRect& /*clipBounds*/, + const SkImageInfo&) { + return nullptr; + } + + // TODO: Delete this once Android gets updated to take the clipBounds version above. + virtual std::unique_ptr onSnapGpuDrawHandler(GrBackendApi, const SkMatrix&) { + return nullptr; + } + + /** + * Default implementation calls onDraw() with a canvas that records into a picture. Subclasses + * may override if they have a more efficient way to return a picture for the current state + * of their drawable. Note: this picture must draw the same as what would be drawn from + * onDraw(). + */ + virtual sk_sp onMakePictureSnapshot(); + +private: + int32_t fGenerationID; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkExecutor.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkExecutor.h new file mode 100644 index 00000000000..88e2ca6e52e --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkExecutor.h @@ -0,0 +1,41 @@ +/* + * Copyright 2017 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkExecutor_DEFINED +#define SkExecutor_DEFINED + +#include +#include +#include "include/core/SkTypes.h" + +class SK_API SkExecutor { +public: + virtual ~SkExecutor(); + + // Create a thread pool SkExecutor with a fixed thread count, by default the number of cores. + static std::unique_ptr MakeFIFOThreadPool(int threads = 0, + bool allowBorrowing = true); + static std::unique_ptr MakeLIFOThreadPool(int threads = 0, + bool allowBorrowing = true); + + // There is always a default SkExecutor available by calling SkExecutor::GetDefault(). + static SkExecutor& GetDefault(); + static void SetDefault(SkExecutor*); // Does not take ownership. Not thread safe. + + // Add work to execute. + virtual void add(std::function) = 0; + + // If it makes sense for this executor, use this thread to execute work for a little while. + virtual void borrow() {} + +protected: + SkExecutor() = default; + SkExecutor(const SkExecutor&) = delete; + SkExecutor& operator=(const SkExecutor&) = delete; +}; + +#endif//SkExecutor_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFlattenable.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFlattenable.h new file mode 100644 index 00000000000..892a5933a27 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFlattenable.h @@ -0,0 +1,115 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFlattenable_DEFINED +#define SkFlattenable_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" + +#include + +class SkData; +class SkReadBuffer; +class SkWriteBuffer; +struct SkDeserialProcs; +struct SkSerialProcs; + +/** \class SkFlattenable + + SkFlattenable is the base class for objects that need to be flattened + into a data stream for either transport or as part of the key to the + font cache. + */ +class SK_API SkFlattenable : public SkRefCnt { +public: + enum Type { + kSkColorFilter_Type, + kSkBlender_Type, + kSkDrawable_Type, + kSkDrawLooper_Type, // no longer supported by Skia + kSkImageFilter_Type, + kSkMaskFilter_Type, + kSkPathEffect_Type, + kSkShader_Type, + }; + + typedef sk_sp (*Factory)(SkReadBuffer&); + + SkFlattenable() {} + + /** Implement this to return a factory function pointer that can be called + to recreate your class given a buffer (previously written to by your + override of flatten(). + */ + virtual Factory getFactory() const = 0; + + /** + * Returns the name of the object's class. + */ + virtual const char* getTypeName() const = 0; + + static Factory NameToFactory(const char name[]); + static const char* FactoryToName(Factory); + + static void Register(const char name[], Factory); + + /** + * Override this if your subclass needs to record data that it will need to recreate itself + * from its CreateProc (returned by getFactory()). + * + * DEPRECATED public : will move to protected ... use serialize() instead + */ + virtual void flatten(SkWriteBuffer&) const {} + + virtual Type getFlattenableType() const = 0; + + // + // public ways to serialize / deserialize + // + sk_sp serialize(const SkSerialProcs* = nullptr) const; + size_t serialize(void* memory, size_t memory_size, + const SkSerialProcs* = nullptr) const; + static sk_sp Deserialize(Type, const void* data, size_t length, + const SkDeserialProcs* procs = nullptr); + +protected: + class PrivateInitializer { + public: + static void InitEffects(); + static void InitImageFilters(); + }; + +private: + static void RegisterFlattenablesIfNeeded(); + static void Finalize(); + + friend class SkGraphics; + + using INHERITED = SkRefCnt; +}; + +#if defined(SK_DISABLE_EFFECT_DESERIALIZATION) + #define SK_REGISTER_FLATTENABLE(type) do{}while(false) + + #define SK_FLATTENABLE_HOOKS(type) \ + static sk_sp CreateProc(SkReadBuffer&); \ + friend class SkFlattenable::PrivateInitializer; \ + Factory getFactory() const override { return nullptr; } \ + const char* getTypeName() const override { return #type; } +#else + #define SK_REGISTER_FLATTENABLE(type) \ + SkFlattenable::Register(#type, type::CreateProc) + + #define SK_FLATTENABLE_HOOKS(type) \ + static sk_sp CreateProc(SkReadBuffer&); \ + friend class SkFlattenable::PrivateInitializer; \ + Factory getFactory() const override { return type::CreateProc; } \ + const char* getTypeName() const override { return #type; } +#endif + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFont.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFont.h new file mode 100644 index 00000000000..e0c3533fd68 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFont.h @@ -0,0 +1,539 @@ +/* + * Copyright 2014 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFont_DEFINED +#define SkFont_DEFINED + +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypeface.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkTo.h" +#include "include/private/base/SkTypeTraits.h" + +#include +#include +#include +#include + +class SkMatrix; +class SkPaint; +class SkPath; +enum class SkFontHinting; +enum class SkTextEncoding; +struct SkFontMetrics; +struct SkPoint; + +/** \class SkFont + SkFont controls options applied when drawing and measuring text. +*/ +class SK_API SkFont { +public: + /** Whether edge pixels draw opaque or with partial transparency. + */ + enum class Edging { + kAlias, //!< no transparent pixels on glyph edges + kAntiAlias, //!< may have transparent pixels on glyph edges + kSubpixelAntiAlias, //!< glyph positioned in pixel using transparency + }; + + /** Constructs SkFont with default values. + + @return default initialized SkFont + */ + SkFont(); + + /** Constructs SkFont with default values with SkTypeface and size in points. + + @param typeface font and style used to draw and measure text + @param size typographic height of text + @return initialized SkFont + */ + SkFont(sk_sp typeface, SkScalar size); + + /** Constructs SkFont with default values with SkTypeface. + + @param typeface font and style used to draw and measure text + @return initialized SkFont + */ + explicit SkFont(sk_sp typeface); + + + /** Constructs SkFont with default values with SkTypeface and size in points, + horizontal scale, and horizontal skew. Horizontal scale emulates condensed + and expanded fonts. Horizontal skew emulates oblique fonts. + + @param typeface font and style used to draw and measure text + @param size typographic height of text + @param scaleX text horizontal scale + @param skewX additional shear on x-axis relative to y-axis + @return initialized SkFont + */ + SkFont(sk_sp typeface, SkScalar size, SkScalar scaleX, SkScalar skewX); + + + /** Compares SkFont and font, and returns true if they are equivalent. + May return false if SkTypeface has identical contents but different pointers. + + @param font font to compare + @return true if SkFont pair are equivalent + */ + bool operator==(const SkFont& font) const; + + /** Compares SkFont and font, and returns true if they are not equivalent. + May return true if SkTypeface has identical contents but different pointers. + + @param font font to compare + @return true if SkFont pair are not equivalent + */ + bool operator!=(const SkFont& font) const { return !(*this == font); } + + /** If true, instructs the font manager to always hint glyphs. + Returned value is only meaningful if platform uses FreeType as the font manager. + + @return true if all glyphs are hinted + */ + bool isForceAutoHinting() const { return SkToBool(fFlags & kForceAutoHinting_PrivFlag); } + + /** Returns true if font engine may return glyphs from font bitmaps instead of from outlines. + + @return true if glyphs may be font bitmaps + */ + bool isEmbeddedBitmaps() const { return SkToBool(fFlags & kEmbeddedBitmaps_PrivFlag); } + + /** Returns true if glyphs may be drawn at sub-pixel offsets. + + @return true if glyphs may be drawn at sub-pixel offsets. + */ + bool isSubpixel() const { return SkToBool(fFlags & kSubpixel_PrivFlag); } + + /** Returns true if font and glyph metrics are requested to be linearly scalable. + + @return true if font and glyph metrics are requested to be linearly scalable. + */ + bool isLinearMetrics() const { return SkToBool(fFlags & kLinearMetrics_PrivFlag); } + + /** Returns true if bold is approximated by increasing the stroke width when creating glyph + bitmaps from outlines. + + @return bold is approximated through stroke width + */ + bool isEmbolden() const { return SkToBool(fFlags & kEmbolden_PrivFlag); } + + /** Returns true if baselines will be snapped to pixel positions when the current transformation + matrix is axis aligned. + + @return baselines may be snapped to pixels + */ + bool isBaselineSnap() const { return SkToBool(fFlags & kBaselineSnap_PrivFlag); } + + /** Sets whether to always hint glyphs. + If forceAutoHinting is set, instructs the font manager to always hint glyphs. + + Only affects platforms that use FreeType as the font manager. + + @param forceAutoHinting setting to always hint glyphs + */ + void setForceAutoHinting(bool forceAutoHinting); + + /** Requests, but does not require, to use bitmaps in fonts instead of outlines. + + @param embeddedBitmaps setting to use bitmaps in fonts + */ + void setEmbeddedBitmaps(bool embeddedBitmaps); + + /** Requests, but does not require, that glyphs respect sub-pixel positioning. + + @param subpixel setting for sub-pixel positioning + */ + void setSubpixel(bool subpixel); + + /** Requests, but does not require, linearly scalable font and glyph metrics. + + For outline fonts 'true' means font and glyph metrics should ignore hinting and rounding. + Note that some bitmap formats may not be able to scale linearly and will ignore this flag. + + @param linearMetrics setting for linearly scalable font and glyph metrics. + */ + void setLinearMetrics(bool linearMetrics); + + /** Increases stroke width when creating glyph bitmaps to approximate a bold typeface. + + @param embolden setting for bold approximation + */ + void setEmbolden(bool embolden); + + /** Requests that baselines be snapped to pixels when the current transformation matrix is axis + aligned. + + @param baselineSnap setting for baseline snapping to pixels + */ + void setBaselineSnap(bool baselineSnap); + + /** Whether edge pixels draw opaque or with partial transparency. + */ + Edging getEdging() const { return (Edging)fEdging; } + + /** Requests, but does not require, that edge pixels draw opaque or with + partial transparency. + */ + void setEdging(Edging edging); + + /** Sets level of glyph outline adjustment. + Does not check for valid values of hintingLevel. + */ + void setHinting(SkFontHinting hintingLevel); + + /** Returns level of glyph outline adjustment. + */ + SkFontHinting getHinting() const { return (SkFontHinting)fHinting; } + + /** Returns a font with the same attributes of this font, but with the specified size. + Returns nullptr if size is less than zero, infinite, or NaN. + + @param size typographic height of text + @return initialized SkFont + */ + SkFont makeWithSize(SkScalar size) const; + + /** Does not alter SkTypeface SkRefCnt. + + @return non-null SkTypeface + */ + SkTypeface* getTypeface() const { + SkASSERT(fTypeface); + return fTypeface.get(); + } + + /** Returns text size in points. + + @return typographic height of text + */ + SkScalar getSize() const { return fSize; } + + /** Returns text scale on x-axis. + Default value is 1. + + @return text horizontal scale + */ + SkScalar getScaleX() const { return fScaleX; } + + /** Returns text skew on x-axis. + Default value is zero. + + @return additional shear on x-axis relative to y-axis + */ + SkScalar getSkewX() const { return fSkewX; } + + /** Increases SkTypeface SkRefCnt by one. + + @return A non-null SkTypeface. + */ + sk_sp refTypeface() const { + SkASSERT(fTypeface); + return fTypeface; + } + + /** Sets SkTypeface to typeface, decreasing SkRefCnt of the previous SkTypeface. + Pass nullptr to clear SkTypeface and use an empty typeface (which draws nothing). + Increments tf SkRefCnt by one. + + @param tf font and style used to draw text + */ + void setTypeface(sk_sp tf); + + /** Sets text size in points. + Has no effect if textSize is not greater than or equal to zero. + + @param textSize typographic height of text + */ + void setSize(SkScalar textSize); + + /** Sets text scale on x-axis. + Default value is 1. + + @param scaleX text horizontal scale + */ + void setScaleX(SkScalar scaleX); + + /** Sets text skew on x-axis. + Default value is zero. + + @param skewX additional shear on x-axis relative to y-axis + */ + void setSkewX(SkScalar skewX); + + /** Converts text into glyph indices. + Returns the number of glyph indices represented by text. + SkTextEncoding specifies how text represents characters or glyphs. + glyphs may be nullptr, to compute the glyph count. + + Does not check text for valid character codes or valid glyph indices. + + If byteLength equals zero, returns zero. + If byteLength includes a partial character, the partial character is ignored. + + If encoding is SkTextEncoding::kUTF8 and text contains an invalid UTF-8 sequence, + zero is returned. + + When encoding is SkTextEncoding::kUTF8, SkTextEncoding::kUTF16, or + SkTextEncoding::kUTF32; then each Unicode codepoint is mapped to a + single glyph. This function uses the default character-to-glyph + mapping from the SkTypeface and maps characters not found in the + SkTypeface to zero. + + If maxGlyphCount is not sufficient to store all the glyphs, no glyphs are copied. + The total glyph count is returned for subsequent buffer reallocation. + + @param text character storage encoded with SkTextEncoding + @param byteLength length of character storage in bytes + @param glyphs storage for glyph indices; may be nullptr + @param maxGlyphCount storage capacity + @return number of glyphs represented by text of length byteLength + */ + int textToGlyphs(const void* text, size_t byteLength, SkTextEncoding encoding, + SkGlyphID glyphs[], int maxGlyphCount) const; + + /** Returns glyph index for Unicode character. + + If the character is not supported by the SkTypeface, returns 0. + + @param uni Unicode character + @return glyph index + */ + SkGlyphID unicharToGlyph(SkUnichar uni) const; + + void unicharsToGlyphs(const SkUnichar uni[], int count, SkGlyphID glyphs[]) const; + + /** Returns number of glyphs represented by text. + + If encoding is SkTextEncoding::kUTF8, SkTextEncoding::kUTF16, or + SkTextEncoding::kUTF32; then each Unicode codepoint is mapped to a + single glyph. + + @param text character storage encoded with SkTextEncoding + @param byteLength length of character storage in bytes + @return number of glyphs represented by text of length byteLength + */ + int countText(const void* text, size_t byteLength, SkTextEncoding encoding) const { + return this->textToGlyphs(text, byteLength, encoding, nullptr, 0); + } + + /** Returns the advance width of text. + The advance is the normal distance to move before drawing additional text. + Returns the bounding box of text if bounds is not nullptr. + + @param text character storage encoded with SkTextEncoding + @param byteLength length of character storage in bytes + @param bounds returns bounding box relative to (0, 0) if not nullptr + @return the sum of the default advance widths + */ + SkScalar measureText(const void* text, size_t byteLength, SkTextEncoding encoding, + SkRect* bounds = nullptr) const { + return this->measureText(text, byteLength, encoding, bounds, nullptr); + } + + /** Returns the advance width of text. + The advance is the normal distance to move before drawing additional text. + Returns the bounding box of text if bounds is not nullptr. The paint + stroke settings, mask filter, or path effect may modify the bounds. + + @param text character storage encoded with SkTextEncoding + @param byteLength length of character storage in bytes + @param bounds returns bounding box relative to (0, 0) if not nullptr + @param paint optional; may be nullptr + @return the sum of the default advance widths + */ + SkScalar measureText(const void* text, size_t byteLength, SkTextEncoding encoding, + SkRect* bounds, const SkPaint* paint) const; + + /** DEPRECATED + Retrieves the advance and bounds for each glyph in glyphs. + Both widths and bounds may be nullptr. + If widths is not nullptr, widths must be an array of count entries. + if bounds is not nullptr, bounds must be an array of count entries. + + @param glyphs array of glyph indices to be measured + @param count number of glyphs + @param widths returns text advances for each glyph; may be nullptr + @param bounds returns bounds for each glyph relative to (0, 0); may be nullptr + */ + void getWidths(const SkGlyphID glyphs[], int count, SkScalar widths[], SkRect bounds[]) const { + this->getWidthsBounds(glyphs, count, widths, bounds, nullptr); + } + + // DEPRECATED + void getWidths(const SkGlyphID glyphs[], int count, SkScalar widths[], std::nullptr_t) const { + this->getWidths(glyphs, count, widths); + } + + /** Retrieves the advance and bounds for each glyph in glyphs. + Both widths and bounds may be nullptr. + If widths is not nullptr, widths must be an array of count entries. + if bounds is not nullptr, bounds must be an array of count entries. + + @param glyphs array of glyph indices to be measured + @param count number of glyphs + @param widths returns text advances for each glyph + */ + void getWidths(const SkGlyphID glyphs[], int count, SkScalar widths[]) const { + this->getWidthsBounds(glyphs, count, widths, nullptr, nullptr); + } + + /** Retrieves the advance and bounds for each glyph in glyphs. + Both widths and bounds may be nullptr. + If widths is not nullptr, widths must be an array of count entries. + if bounds is not nullptr, bounds must be an array of count entries. + + @param glyphs array of glyph indices to be measured + @param count number of glyphs + @param widths returns text advances for each glyph; may be nullptr + @param bounds returns bounds for each glyph relative to (0, 0); may be nullptr + @param paint optional, specifies stroking, SkPathEffect and SkMaskFilter + */ + void getWidthsBounds(const SkGlyphID glyphs[], int count, SkScalar widths[], SkRect bounds[], + const SkPaint* paint) const; + + + /** Retrieves the bounds for each glyph in glyphs. + bounds must be an array of count entries. + If paint is not nullptr, its stroking, SkPathEffect, and SkMaskFilter fields are respected. + + @param glyphs array of glyph indices to be measured + @param count number of glyphs + @param bounds returns bounds for each glyph relative to (0, 0); may be nullptr + @param paint optional, specifies stroking, SkPathEffect, and SkMaskFilter + */ + void getBounds(const SkGlyphID glyphs[], int count, SkRect bounds[], + const SkPaint* paint) const { + this->getWidthsBounds(glyphs, count, nullptr, bounds, paint); + } + + /** Retrieves the positions for each glyph, beginning at the specified origin. The caller + must allocated at least count number of elements in the pos[] array. + + @param glyphs array of glyph indices to be positioned + @param count number of glyphs + @param pos returns glyphs positions + @param origin location of the first glyph. Defaults to {0, 0}. + */ + void getPos(const SkGlyphID glyphs[], int count, SkPoint pos[], SkPoint origin = {0, 0}) const; + + /** Retrieves the x-positions for each glyph, beginning at the specified origin. The caller + must allocated at least count number of elements in the xpos[] array. + + @param glyphs array of glyph indices to be positioned + @param count number of glyphs + @param xpos returns glyphs x-positions + @param origin x-position of the first glyph. Defaults to 0. + */ + void getXPos(const SkGlyphID glyphs[], int count, SkScalar xpos[], SkScalar origin = 0) const; + + /** Returns intervals [start, end] describing lines parallel to the advance that intersect + * with the glyphs. + * + * @param glyphs the glyphs to intersect + * @param count the number of glyphs and positions + * @param pos the position of each glyph + * @param top the top of the line intersecting + * @param bottom the bottom of the line intersecting + @return array of pairs of x values [start, end]. May be empty. + */ + std::vector getIntercepts(const SkGlyphID glyphs[], int count, const SkPoint pos[], + SkScalar top, SkScalar bottom, + const SkPaint* = nullptr) const; + + /** Modifies path to be the outline of the glyph. + If the glyph has an outline, modifies path to be the glyph's outline and returns true. + The glyph outline may be empty. Degenerate contours in the glyph outline will be skipped. + If glyph is described by a bitmap, returns false and ignores path parameter. + + @param glyphID index of glyph + @param path pointer to existing SkPath + @return true if glyphID is described by path + */ + bool getPath(SkGlyphID glyphID, SkPath* path) const; + + /** Returns path corresponding to glyph array. + + @param glyphIDs array of glyph indices + @param count number of glyphs + @param glyphPathProc function returning one glyph description as path + @param ctx function context + */ + void getPaths(const SkGlyphID glyphIDs[], int count, + void (*glyphPathProc)(const SkPath* pathOrNull, const SkMatrix& mx, void* ctx), + void* ctx) const; + + /** Returns SkFontMetrics associated with SkTypeface. + The return value is the recommended spacing between lines: the sum of metrics + descent, ascent, and leading. + If metrics is not nullptr, SkFontMetrics is copied to metrics. + Results are scaled by text size but does not take into account + dimensions required by text scale, text skew, fake bold, + style stroke, and SkPathEffect. + + @param metrics storage for SkFontMetrics; may be nullptr + @return recommended spacing between lines + */ + SkScalar getMetrics(SkFontMetrics* metrics) const; + + /** Returns the recommended spacing between lines: the sum of metrics + descent, ascent, and leading. + Result is scaled by text size but does not take into account + dimensions required by stroking and SkPathEffect. + Returns the same result as getMetrics(). + + @return recommended spacing between lines + */ + SkScalar getSpacing() const { return this->getMetrics(nullptr); } + + /** Dumps fields of the font to SkDebugf. May change its output over time, so clients should + * not rely on this for anything specific. Used to aid in debugging. + */ + void dump() const; + + using sk_is_trivially_relocatable = std::true_type; + +private: + enum PrivFlags { + kForceAutoHinting_PrivFlag = 1 << 0, + kEmbeddedBitmaps_PrivFlag = 1 << 1, + kSubpixel_PrivFlag = 1 << 2, + kLinearMetrics_PrivFlag = 1 << 3, + kEmbolden_PrivFlag = 1 << 4, + kBaselineSnap_PrivFlag = 1 << 5, + }; + + static constexpr unsigned kAllFlags = kForceAutoHinting_PrivFlag + | kEmbeddedBitmaps_PrivFlag + | kSubpixel_PrivFlag + | kLinearMetrics_PrivFlag + | kEmbolden_PrivFlag + | kBaselineSnap_PrivFlag; + + sk_sp fTypeface; + SkScalar fSize; + SkScalar fScaleX; + SkScalar fSkewX; + uint8_t fFlags; + uint8_t fEdging; + uint8_t fHinting; + + static_assert(::sk_is_trivially_relocatable::value); + + SkScalar setupForAsPaths(SkPaint*); + bool hasSomeAntiAliasing() const; + + friend class SkFontPriv; + friend class SkGlyphRunListPainterCPU; + friend class SkStrikeSpec; + friend class SkRemoteGlyphCacheTest; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontArguments.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontArguments.h new file mode 100644 index 00000000000..5ab8d2e182b --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontArguments.h @@ -0,0 +1,94 @@ +/* + * Copyright 2017 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFontArguments_DEFINED +#define SkFontArguments_DEFINED + +#include "include/core/SkColor.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" + +/** Represents a set of actual arguments for a font. */ +struct SkFontArguments { + struct VariationPosition { + struct Coordinate { + SkFourByteTag axis; + float value; + }; + const Coordinate* coordinates; + int coordinateCount; + }; + + /** Specify a palette to use and overrides for palette entries. + * + * `overrides` is a list of pairs of palette entry index and color. + * The overriden palette entries will use the associated color. + * Override pairs with palette entry indices out of range will not be applied. + * Later override entries override earlier ones. + */ + struct Palette { + struct Override { + uint16_t index; + SkColor color; + }; + int index; + const Override* overrides; + int overrideCount; + }; + + SkFontArguments() + : fCollectionIndex(0) + , fVariationDesignPosition{nullptr, 0} + , fPalette{0, nullptr, 0} {} + + /** Specify the index of the desired font. + * + * Font formats like ttc, dfont, cff, cid, pfr, t42, t1, and fon may actually be indexed + * collections of fonts. + */ + SkFontArguments& setCollectionIndex(int collectionIndex) { + fCollectionIndex = collectionIndex; + return *this; + } + + /** Specify a position in the variation design space. + * + * Any axis not specified will use the default value. + * Any specified axis not actually present in the font will be ignored. + * + * @param position not copied. The value must remain valid for life of SkFontArguments. + */ + SkFontArguments& setVariationDesignPosition(VariationPosition position) { + fVariationDesignPosition.coordinates = position.coordinates; + fVariationDesignPosition.coordinateCount = position.coordinateCount; + return *this; + } + + int getCollectionIndex() const { + return fCollectionIndex; + } + + VariationPosition getVariationDesignPosition() const { + return fVariationDesignPosition; + } + + SkFontArguments& setPalette(Palette palette) { + fPalette.index = palette.index; + fPalette.overrides = palette.overrides; + fPalette.overrideCount = palette.overrideCount; + return *this; + } + + Palette getPalette() const { return fPalette; } + +private: + int fCollectionIndex; + VariationPosition fVariationDesignPosition; + Palette fPalette; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontMetrics.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontMetrics.h new file mode 100644 index 00000000000..0686246ad10 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontMetrics.h @@ -0,0 +1,139 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFontMetrics_DEFINED +#define SkFontMetrics_DEFINED + +#include "include/core/SkScalar.h" +#include "include/private/base/SkTo.h" + +/** \class SkFontMetrics + The metrics of an SkFont. + The metric values are consistent with the Skia y-down coordinate system. + */ +struct SK_API SkFontMetrics { + bool operator==(const SkFontMetrics& that) const { + return + this->fFlags == that.fFlags && + this->fTop == that.fTop && + this->fAscent == that.fAscent && + this->fDescent == that.fDescent && + this->fBottom == that.fBottom && + this->fLeading == that.fLeading && + this->fAvgCharWidth == that.fAvgCharWidth && + this->fMaxCharWidth == that.fMaxCharWidth && + this->fXMin == that.fXMin && + this->fXMax == that.fXMax && + this->fXHeight == that.fXHeight && + this->fCapHeight == that.fCapHeight && + this->fUnderlineThickness == that.fUnderlineThickness && + this->fUnderlinePosition == that.fUnderlinePosition && + this->fStrikeoutThickness == that.fStrikeoutThickness && + this->fStrikeoutPosition == that.fStrikeoutPosition; + } + + /** \enum FontMetricsFlags + FontMetricsFlags indicate when certain metrics are valid; + the underline or strikeout metrics may be valid and zero. + Fonts with embedded bitmaps may not have valid underline or strikeout metrics. + */ + enum FontMetricsFlags { + kUnderlineThicknessIsValid_Flag = 1 << 0, //!< set if fUnderlineThickness is valid + kUnderlinePositionIsValid_Flag = 1 << 1, //!< set if fUnderlinePosition is valid + kStrikeoutThicknessIsValid_Flag = 1 << 2, //!< set if fStrikeoutThickness is valid + kStrikeoutPositionIsValid_Flag = 1 << 3, //!< set if fStrikeoutPosition is valid + kBoundsInvalid_Flag = 1 << 4, //!< set if fTop, fBottom, fXMin, fXMax invalid + }; + + uint32_t fFlags; //!< FontMetricsFlags indicating which metrics are valid + SkScalar fTop; //!< greatest extent above origin of any glyph bounding box, typically negative; deprecated with variable fonts + SkScalar fAscent; //!< distance to reserve above baseline, typically negative + SkScalar fDescent; //!< distance to reserve below baseline, typically positive + SkScalar fBottom; //!< greatest extent below origin of any glyph bounding box, typically positive; deprecated with variable fonts + SkScalar fLeading; //!< distance to add between lines, typically positive or zero + SkScalar fAvgCharWidth; //!< average character width, zero if unknown + SkScalar fMaxCharWidth; //!< maximum character width, zero if unknown + SkScalar fXMin; //!< greatest extent to left of origin of any glyph bounding box, typically negative; deprecated with variable fonts + SkScalar fXMax; //!< greatest extent to right of origin of any glyph bounding box, typically positive; deprecated with variable fonts + SkScalar fXHeight; //!< height of lower-case 'x', zero if unknown, typically negative + SkScalar fCapHeight; //!< height of an upper-case letter, zero if unknown, typically negative + SkScalar fUnderlineThickness; //!< underline thickness + SkScalar fUnderlinePosition; //!< distance from baseline to top of stroke, typically positive + SkScalar fStrikeoutThickness; //!< strikeout thickness + SkScalar fStrikeoutPosition; //!< distance from baseline to bottom of stroke, typically negative + + /** Returns true if SkFontMetrics has a valid underline thickness, and sets + thickness to that value. If the underline thickness is not valid, + return false, and ignore thickness. + + @param thickness storage for underline width + @return true if font specifies underline width + */ + bool hasUnderlineThickness(SkScalar* thickness) const { + if (SkToBool(fFlags & kUnderlineThicknessIsValid_Flag)) { + *thickness = fUnderlineThickness; + return true; + } + return false; + } + + /** Returns true if SkFontMetrics has a valid underline position, and sets + position to that value. If the underline position is not valid, + return false, and ignore position. + + @param position storage for underline position + @return true if font specifies underline position + */ + bool hasUnderlinePosition(SkScalar* position) const { + if (SkToBool(fFlags & kUnderlinePositionIsValid_Flag)) { + *position = fUnderlinePosition; + return true; + } + return false; + } + + /** Returns true if SkFontMetrics has a valid strikeout thickness, and sets + thickness to that value. If the underline thickness is not valid, + return false, and ignore thickness. + + @param thickness storage for strikeout width + @return true if font specifies strikeout width + */ + bool hasStrikeoutThickness(SkScalar* thickness) const { + if (SkToBool(fFlags & kStrikeoutThicknessIsValid_Flag)) { + *thickness = fStrikeoutThickness; + return true; + } + return false; + } + + /** Returns true if SkFontMetrics has a valid strikeout position, and sets + position to that value. If the underline position is not valid, + return false, and ignore position. + + @param position storage for strikeout position + @return true if font specifies strikeout position + */ + bool hasStrikeoutPosition(SkScalar* position) const { + if (SkToBool(fFlags & kStrikeoutPositionIsValid_Flag)) { + *position = fStrikeoutPosition; + return true; + } + return false; + } + + /** Returns true if SkFontMetrics has a valid fTop, fBottom, fXMin, and fXMax. + If the bounds are not valid, return false. + + @return true if font specifies maximum glyph bounds + */ + bool hasBounds() const { + return !SkToBool(fFlags & kBoundsInvalid_Flag); + } +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontMgr.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontMgr.h new file mode 100644 index 00000000000..48f49f68458 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontMgr.h @@ -0,0 +1,143 @@ +/* + * Copyright 2013 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFontMgr_DEFINED +#define SkFontMgr_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" + +#include + +class SkData; +class SkFontStyle; +class SkStreamAsset; +class SkString; +class SkTypeface; +struct SkFontArguments; + +class SK_API SkFontStyleSet : public SkRefCnt { +public: + virtual int count() = 0; + virtual void getStyle(int index, SkFontStyle*, SkString* style) = 0; + virtual sk_sp createTypeface(int index) = 0; + virtual sk_sp matchStyle(const SkFontStyle& pattern) = 0; + + static sk_sp CreateEmpty(); + +protected: + sk_sp matchStyleCSS3(const SkFontStyle& pattern); +}; + +class SK_API SkFontMgr : public SkRefCnt { +public: + int countFamilies() const; + void getFamilyName(int index, SkString* familyName) const; + sk_sp createStyleSet(int index) const; + + /** + * The caller must call unref() on the returned object. + * Never returns NULL; will return an empty set if the name is not found. + * + * Passing nullptr as the parameter will return the default system family. + * Note that most systems don't have a default system family, so passing nullptr will often + * result in the empty set. + * + * It is possible that this will return a style set not accessible from + * createStyleSet(int) due to hidden or auto-activated fonts. + */ + sk_sp matchFamily(const char familyName[]) const; + + /** + * Find the closest matching typeface to the specified familyName and style + * and return a ref to it. The caller must call unref() on the returned + * object. Will return nullptr if no 'good' match is found. + * + * Passing |nullptr| as the parameter for |familyName| will return the + * default system font. + * + * It is possible that this will return a style set not accessible from + * createStyleSet(int) or matchFamily(const char[]) due to hidden or + * auto-activated fonts. + */ + sk_sp matchFamilyStyle(const char familyName[], const SkFontStyle&) const; + + /** + * Use the system fallback to find a typeface for the given character. + * Note that bcp47 is a combination of ISO 639, 15924, and 3166-1 codes, + * so it is fine to just pass a ISO 639 here. + * + * Will return NULL if no family can be found for the character + * in the system fallback. + * + * Passing |nullptr| as the parameter for |familyName| will return the + * default system font. + * + * bcp47[0] is the least significant fallback, bcp47[bcp47Count-1] is the + * most significant. If no specified bcp47 codes match, any font with the + * requested character will be matched. + */ + sk_sp matchFamilyStyleCharacter(const char familyName[], const SkFontStyle&, + const char* bcp47[], int bcp47Count, + SkUnichar character) const; + + /** + * Create a typeface for the specified data and TTC index (pass 0 for none) + * or NULL if the data is not recognized. The caller must call unref() on + * the returned object if it is not null. + */ + sk_sp makeFromData(sk_sp, int ttcIndex = 0) const; + + /** + * Create a typeface for the specified stream and TTC index + * (pass 0 for none) or NULL if the stream is not recognized. The caller + * must call unref() on the returned object if it is not null. + */ + sk_sp makeFromStream(std::unique_ptr, int ttcIndex = 0) const; + + /* Experimental, API subject to change. */ + sk_sp makeFromStream(std::unique_ptr, const SkFontArguments&) const; + + /** + * Create a typeface for the specified fileName and TTC index + * (pass 0 for none) or NULL if the file is not found, or its contents are + * not recognized. The caller must call unref() on the returned object + * if it is not null. + */ + sk_sp makeFromFile(const char path[], int ttcIndex = 0) const; + + sk_sp legacyMakeTypeface(const char familyName[], SkFontStyle style) const; + + /* Returns an empty font manager without any typeface dependencies */ + static sk_sp RefEmpty(); + +protected: + virtual int onCountFamilies() const = 0; + virtual void onGetFamilyName(int index, SkString* familyName) const = 0; + virtual sk_sp onCreateStyleSet(int index)const = 0; + + /** May return NULL if the name is not found. */ + virtual sk_sp onMatchFamily(const char familyName[]) const = 0; + + virtual sk_sp onMatchFamilyStyle(const char familyName[], + const SkFontStyle&) const = 0; + virtual sk_sp onMatchFamilyStyleCharacter(const char familyName[], + const SkFontStyle&, + const char* bcp47[], int bcp47Count, + SkUnichar character) const = 0; + + virtual sk_sp onMakeFromData(sk_sp, int ttcIndex) const = 0; + virtual sk_sp onMakeFromStreamIndex(std::unique_ptr, + int ttcIndex) const = 0; + virtual sk_sp onMakeFromStreamArgs(std::unique_ptr, + const SkFontArguments&) const = 0; + virtual sk_sp onMakeFromFile(const char path[], int ttcIndex) const = 0; + + virtual sk_sp onLegacyMakeTypeface(const char familyName[], SkFontStyle) const = 0; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontParameters.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontParameters.h new file mode 100644 index 00000000000..ae4f1d68b6c --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontParameters.h @@ -0,0 +1,42 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFontParameters_DEFINED +#define SkFontParameters_DEFINED + +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" + +struct SkFontParameters { + struct Variation { + // Parameters in a variation font axis. + struct Axis { + constexpr Axis() : tag(0), min(0), def(0), max(0), flags(0) {} + constexpr Axis(SkFourByteTag tag, float min, float def, float max, bool hidden) : + tag(tag), min(min), def(def), max(max), flags(hidden ? HIDDEN : 0) {} + + // Four character identifier of the font axis (weight, width, slant, italic...). + SkFourByteTag tag; + // Minimum value supported by this axis. + float min; + // Default value set by this axis. + float def; + // Maximum value supported by this axis. The maximum can equal the minimum. + float max; + // Return whether this axis is recommended to be remain hidden in user interfaces. + bool isHidden() const { return flags & HIDDEN; } + // Set this axis to be remain hidden in user interfaces. + void setHidden(bool hidden) { flags = hidden ? (flags | HIDDEN) : (flags & ~HIDDEN); } + private: + static constexpr uint16_t HIDDEN = 0x0001; + // Attributes for a font axis. + uint16_t flags; + }; + }; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontStyle.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontStyle.h new file mode 100644 index 00000000000..be46b53bb28 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontStyle.h @@ -0,0 +1,84 @@ +/* + * Copyright 2013 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFontStyle_DEFINED +#define SkFontStyle_DEFINED + +#include "include/core/SkTypes.h" +#include "include/private/base/SkTPin.h" + +#include + +class SK_API SkFontStyle { +public: + enum Weight { + kInvisible_Weight = 0, + kThin_Weight = 100, + kExtraLight_Weight = 200, + kLight_Weight = 300, + kNormal_Weight = 400, + kMedium_Weight = 500, + kSemiBold_Weight = 600, + kBold_Weight = 700, + kExtraBold_Weight = 800, + kBlack_Weight = 900, + kExtraBlack_Weight = 1000, + }; + + enum Width { + kUltraCondensed_Width = 1, + kExtraCondensed_Width = 2, + kCondensed_Width = 3, + kSemiCondensed_Width = 4, + kNormal_Width = 5, + kSemiExpanded_Width = 6, + kExpanded_Width = 7, + kExtraExpanded_Width = 8, + kUltraExpanded_Width = 9, + }; + + enum Slant { + kUpright_Slant, + kItalic_Slant, + kOblique_Slant, + }; + + constexpr SkFontStyle(int weight, int width, Slant slant) : fValue( + (SkTPin(weight, kInvisible_Weight, kExtraBlack_Weight)) + + (SkTPin(width, kUltraCondensed_Width, kUltraExpanded_Width) << 16) + + (SkTPin(slant, kUpright_Slant, kOblique_Slant) << 24) + ) { } + + constexpr SkFontStyle() : SkFontStyle{kNormal_Weight, kNormal_Width, kUpright_Slant} { } + + bool operator==(const SkFontStyle& rhs) const { + return fValue == rhs.fValue; + } + + int weight() const { return fValue & 0xFFFF; } + int width() const { return (fValue >> 16) & 0xFF; } + Slant slant() const { return (Slant)((fValue >> 24) & 0xFF); } + + static constexpr SkFontStyle Normal() { + return SkFontStyle(kNormal_Weight, kNormal_Width, kUpright_Slant); + } + static constexpr SkFontStyle Bold() { + return SkFontStyle(kBold_Weight, kNormal_Width, kUpright_Slant); + } + static constexpr SkFontStyle Italic() { + return SkFontStyle(kNormal_Weight, kNormal_Width, kItalic_Slant ); + } + static constexpr SkFontStyle BoldItalic() { + return SkFontStyle(kBold_Weight, kNormal_Width, kItalic_Slant ); + } + +private: + friend class SkTypefaceProxyPrototype; // To serialize fValue + int32_t fValue; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontTypes.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontTypes.h new file mode 100644 index 00000000000..76f5dde67fe --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkFontTypes.h @@ -0,0 +1,25 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFontTypes_DEFINED +#define SkFontTypes_DEFINED + +enum class SkTextEncoding { + kUTF8, //!< uses bytes to represent UTF-8 or ASCII + kUTF16, //!< uses two byte words to represent most of Unicode + kUTF32, //!< uses four byte words to represent all of Unicode + kGlyphID, //!< uses two byte words to represent glyph indices +}; + +enum class SkFontHinting { + kNone, //!< glyph outlines unchanged + kSlight, //!< minimal modification to improve constrast + kNormal, //!< glyph outlines modified to improve constrast + kFull, //!< modifies glyph outlines for maximum constrast +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkGraphics.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkGraphics.h new file mode 100644 index 00000000000..58fd16b7341 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkGraphics.h @@ -0,0 +1,169 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkGraphics_DEFINED +#define SkGraphics_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +#include +#include +#include + +class SkData; +class SkImageGenerator; +class SkOpenTypeSVGDecoder; +class SkTraceMemoryDump; + +class SK_API SkGraphics { +public: + /** + * Call this at process initialization time if your environment does not + * permit static global initializers that execute code. + * Init() is thread-safe and idempotent. + */ + static void Init(); + + /** + * Return the max number of bytes that should be used by the font cache. + * If the cache needs to allocate more, it will purge previous entries. + * This max can be changed by calling SetFontCacheLimit(). + */ + static size_t GetFontCacheLimit(); + + /** + * Specify the max number of bytes that should be used by the font cache. + * If the cache needs to allocate more, it will purge previous entries. + * + * This function returns the previous setting, as if GetFontCacheLimit() + * had be called before the new limit was set. + */ + static size_t SetFontCacheLimit(size_t bytes); + + /** + * Return the number of bytes currently used by the font cache. + */ + static size_t GetFontCacheUsed(); + + /** + * Return the number of entries in the font cache. + * A cache "entry" is associated with each typeface + pointSize + matrix. + */ + static int GetFontCacheCountUsed(); + + /** + * Return the current limit to the number of entries in the font cache. + * A cache "entry" is associated with each typeface + pointSize + matrix. + */ + static int GetFontCacheCountLimit(); + + /** + * Set the limit to the number of entries in the font cache, and return + * the previous value. If this new value is lower than the previous, + * it will automatically try to purge entries to meet the new limit. + */ + static int SetFontCacheCountLimit(int count); + + /** + * Return the current limit to the number of entries in the typeface cache. + * A cache "entry" is associated with each typeface. + */ + static int GetTypefaceCacheCountLimit(); + + /** + * Set the limit to the number of entries in the typeface cache, and return + * the previous value. Changes to this only take effect the next time + * each cache object is modified. + */ + static int SetTypefaceCacheCountLimit(int count); + + /** + * For debugging purposes, this will attempt to purge the font cache. It + * does not change the limit, but will cause subsequent font measures and + * draws to be recreated, since they will no longer be in the cache. + */ + static void PurgeFontCache(); + + /** + * If the strike cache is above the cache limit, attempt to purge strikes + * with pinners. This should be called after clients release locks on + * pinned strikes. + */ + static void PurgePinnedFontCache(); + + /** + * This function returns the memory used for temporary images and other resources. + */ + static size_t GetResourceCacheTotalBytesUsed(); + + /** + * These functions get/set the memory usage limit for the resource cache, used for temporary + * bitmaps and other resources. Entries are purged from the cache when the memory useage + * exceeds this limit. + */ + static size_t GetResourceCacheTotalByteLimit(); + static size_t SetResourceCacheTotalByteLimit(size_t newLimit); + + /** + * For debugging purposes, this will attempt to purge the resource cache. It + * does not change the limit. + */ + static void PurgeResourceCache(); + + /** + * When the cachable entry is very lage (e.g. a large scaled bitmap), adding it to the cache + * can cause most/all of the existing entries to be purged. To avoid the, the client can set + * a limit for a single allocation. If a cacheable entry would have been cached, but its size + * exceeds this limit, then we do not attempt to cache it at all. + * + * Zero is the default value, meaning we always attempt to cache entries. + */ + static size_t GetResourceCacheSingleAllocationByteLimit(); + static size_t SetResourceCacheSingleAllocationByteLimit(size_t newLimit); + + /** + * Dumps memory usage of caches using the SkTraceMemoryDump interface. See SkTraceMemoryDump + * for usage of this method. + */ + static void DumpMemoryStatistics(SkTraceMemoryDump* dump); + + /** + * Free as much globally cached memory as possible. This will purge all private caches in Skia, + * including font and image caches. + * + * If there are caches associated with GPU context, those will not be affected by this call. + */ + static void PurgeAllCaches(); + + typedef std::unique_ptr + (*ImageGeneratorFromEncodedDataFactory)(sk_sp); + + /** + * To instantiate images from encoded data, first looks at this runtime function-ptr. If it + * exists, it is called to create an SkImageGenerator from SkData. If there is no function-ptr + * or there is, but it returns NULL, then skia will call its internal default implementation. + * + * Returns the previous factory (which could be NULL). + */ + static ImageGeneratorFromEncodedDataFactory + SetImageGeneratorFromEncodedDataFactory(ImageGeneratorFromEncodedDataFactory); + + /** + * To draw OpenType SVG data, Skia will look at this runtime function pointer. If this function + * pointer is set, the SkTypeface implementations which support OpenType SVG will call this + * function to create an SkOpenTypeSVGDecoder to decode the OpenType SVG and draw it as needed. + * If this function is not set, the SkTypeface implementations will generally not support + * OpenType SVG and attempt to use other glyph representations if available. + */ + using OpenTypeSVGDecoderFactory = + std::unique_ptr (*)(const uint8_t* svg, size_t length); + static OpenTypeSVGDecoderFactory SetOpenTypeSVGDecoderFactory(OpenTypeSVGDecoderFactory); + static OpenTypeSVGDecoderFactory GetOpenTypeSVGDecoderFactory(); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkImage.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkImage.h new file mode 100644 index 00000000000..600f167971b --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkImage.h @@ -0,0 +1,948 @@ +/* + * Copyright 2012 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkImage_DEFINED +#define SkImage_DEFINED + +#include "include/core/SkAlphaType.h" +#include "include/core/SkImageInfo.h" +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkSize.h" +#include "include/private/base/SkAPI.h" + +#include +#include +#include +#include + +class GrDirectContext; +class GrRecordingContext; +class SkBitmap; +class SkColorSpace; +class SkData; +class SkImage; +class SkImageFilter; +class SkImageGenerator; +class SkMatrix; +class SkMipmap; +class SkPaint; +class SkPicture; +class SkPixmap; +class SkShader; +class SkSurfaceProps; +enum SkColorType : int; +enum class SkTextureCompressionType; +enum class SkTileMode; + +struct SkIPoint; +struct SkSamplingOptions; + +namespace skgpu::graphite { class Recorder; } + +namespace SkImages { + +/** Caller data passed to RasterReleaseProc; may be nullptr. */ +using ReleaseContext = void*; +/** Function called when SkImage no longer shares pixels. ReleaseContext is + provided by caller when SkImage is created, and may be nullptr. +*/ +using RasterReleaseProc = void(const void* pixels, ReleaseContext); + +/** Creates a CPU-backed SkImage from bitmap, sharing or copying bitmap pixels. If the bitmap + is marked immutable, and its pixel memory is shareable, it may be shared + instead of copied. + + SkImage is returned if bitmap is valid. Valid SkBitmap parameters include: + dimensions are greater than zero; + each dimension fits in 29 bits; + SkColorType and SkAlphaType are valid, and SkColorType is not kUnknown_SkColorType; + row bytes are large enough to hold one row of pixels; + pixel address is not nullptr. + + @param bitmap SkImageInfo, row bytes, and pixels + @return created SkImage, or nullptr +*/ +SK_API sk_sp RasterFromBitmap(const SkBitmap& bitmap); + +/** Creates a CPU-backed SkImage from compressed data. + + This method will decompress the compressed data and create an image wrapping + it. Any mipmap levels present in the compressed data are discarded. + + @param data compressed data to store in SkImage + @param width width of full SkImage + @param height height of full SkImage + @param type type of compression used + @return created SkImage, or nullptr +*/ +SK_API sk_sp RasterFromCompressedTextureData(sk_sp data, + int width, + int height, + SkTextureCompressionType type); + +/** + * Return a SkImage using the encoded data, but attempts to defer decoding until the + * image is actually used/drawn. This deferral allows the system to cache the result, either on the + * CPU or on the GPU, depending on where the image is drawn. If memory is low, the cache may + * be purged, causing the next draw of the image to have to re-decode. + * + * If alphaType is nullopt, the image's alpha type will be chosen automatically based on the + * image format. Transparent images will default to kPremul_SkAlphaType. If alphaType contains + * kPremul_SkAlphaType or kUnpremul_SkAlphaType, that alpha type will be used. Forcing opaque + * (passing kOpaque_SkAlphaType) is not allowed, and will return nullptr. + * + * If the encoded format is not supported, nullptr is returned. + * + * If possible, clients should use SkCodecs::DeferredImage instead. + * + * @param encoded the encoded data + * @return created SkImage, or nullptr + + example: https://fiddle.skia.org/c/@Image_DeferredFromEncodedData +*/ +SK_API sk_sp DeferredFromEncodedData(sk_sp encoded, + std::optional alphaType = std::nullopt); + +/** Creates SkImage from data returned by imageGenerator. The image data will not be created + (on either the CPU or GPU) until the image is actually drawn. + Generated data is owned by SkImage and may not be shared or accessed. + + SkImage is returned if generator data is valid. Valid data parameters vary by type of data + and platform. + + imageGenerator may wrap SkPicture data, codec data, or custom data. + + @param imageGenerator stock or custom routines to retrieve SkImage + @return created SkImage, or nullptr +*/ +SK_API sk_sp DeferredFromGenerator(std::unique_ptr imageGenerator); + +enum class BitDepth { + kU8, //!< uses 8-bit unsigned int per color component + kF16, //!< uses 16-bit float per color component +}; + +/** Creates SkImage from picture. Returned SkImage width and height are set by dimensions. + SkImage draws picture with matrix and paint, set to bitDepth and colorSpace. + + The Picture data is not turned into an image (CPU or GPU) until it is drawn. + + If matrix is nullptr, draws with identity SkMatrix. If paint is nullptr, draws + with default SkPaint. colorSpace may be nullptr. + + @param picture stream of drawing commands + @param dimensions width and height + @param matrix SkMatrix to rotate, scale, translate, and so on; may be nullptr + @param paint SkPaint to apply transparency, filtering, and so on; may be nullptr + @param bitDepth 8-bit integer or 16-bit float: per component + @param colorSpace range of colors; may be nullptr + @param props props to use when rasterizing the picture + @return created SkImage, or nullptr +*/ +SK_API sk_sp DeferredFromPicture(sk_sp picture, + const SkISize& dimensions, + const SkMatrix* matrix, + const SkPaint* paint, + BitDepth bitDepth, + sk_sp colorSpace, + SkSurfaceProps props); +SK_API sk_sp DeferredFromPicture(sk_sp picture, + const SkISize& dimensions, + const SkMatrix* matrix, + const SkPaint* paint, + BitDepth bitDepth, + sk_sp colorSpace); + +/** Creates a CPU-backed SkImage from pixmap, copying the pixel data. + As a result, pixmap pixels may be modified or deleted without affecting SkImage. + + SkImage is returned if SkPixmap is valid. Valid SkPixmap parameters include: + dimensions are greater than zero; + each dimension fits in 29 bits; + SkColorType and SkAlphaType are valid, and SkColorType is not kUnknown_SkColorType; + row bytes are large enough to hold one row of pixels; + pixel address is not nullptr. + + @param pixmap SkImageInfo, pixel address, and row bytes + @return copy of SkPixmap pixels, or nullptr + + example: https://fiddle.skia.org/c/@Image_RasterFromPixmapCopy +*/ +SK_API sk_sp RasterFromPixmapCopy(const SkPixmap& pixmap); + +/** Creates CPU-backed SkImage from pixmap, sharing SkPixmap pixels. Pixels must remain valid and + unchanged until rasterReleaseProc is called. rasterReleaseProc is passed + releaseContext when SkImage is deleted or no longer refers to pixmap pixels. + + Pass nullptr for rasterReleaseProc to share SkPixmap without requiring a callback + when SkImage is released. Pass nullptr for releaseContext if rasterReleaseProc + does not require state. + + SkImage is returned if pixmap is valid. Valid SkPixmap parameters include: + dimensions are greater than zero; + each dimension fits in 29 bits; + SkColorType and SkAlphaType are valid, and SkColorType is not kUnknown_SkColorType; + row bytes are large enough to hold one row of pixels; + pixel address is not nullptr. + + @param pixmap SkImageInfo, pixel address, and row bytes + @param rasterReleaseProc function called when pixels can be released; or nullptr + @param releaseContext state passed to rasterReleaseProc; or nullptr + @return SkImage sharing pixmap +*/ +SK_API sk_sp RasterFromPixmap(const SkPixmap& pixmap, + RasterReleaseProc rasterReleaseProc, + ReleaseContext releaseContext); + +/** Creates CPU-backed SkImage from pixel data described by info. + The pixels data will *not* be copied. + + SkImage is returned if SkImageInfo is valid. Valid SkImageInfo parameters include: + dimensions are greater than zero; + each dimension fits in 29 bits; + SkColorType and SkAlphaType are valid, and SkColorType is not kUnknown_SkColorType; + rowBytes are large enough to hold one row of pixels; + pixels is not nullptr, and contains enough data for SkImage. + + @param info contains width, height, SkAlphaType, SkColorType, SkColorSpace + @param pixels address or pixel storage + @param rowBytes size of pixel row or larger + @return SkImage sharing pixels, or nullptr +*/ +SK_API sk_sp RasterFromData(const SkImageInfo& info, + sk_sp pixels, + size_t rowBytes); + +/** Creates a filtered SkImage on the CPU. filter processes the src image, potentially changing + the color, position, and size. subset is the bounds of src that are processed + by filter. clipBounds is the expected bounds of the filtered SkImage. outSubset + is required storage for the actual bounds of the filtered SkImage. offset is + required storage for translation of returned SkImage. + + Returns nullptr a filtered result could not be created. If nullptr is returned, outSubset + and offset are undefined. + + Useful for animation of SkImageFilter that varies size from frame to frame. + outSubset describes the valid bounds of returned image. offset translates the returned SkImage + to keep subsequent animation frames aligned with respect to each other. + + @param src the image to be filtered + @param filter the image filter to be applied + @param subset bounds of SkImage processed by filter + @param clipBounds expected bounds of filtered SkImage + @param outSubset storage for returned SkImage bounds + @param offset storage for returned SkImage translation + @return filtered SkImage, or nullptr +*/ +SK_API sk_sp MakeWithFilter(sk_sp src, + const SkImageFilter* filter, + const SkIRect& subset, + const SkIRect& clipBounds, + SkIRect* outSubset, + SkIPoint* offset); + +} // namespace SkImages + +/** \class SkImage + SkImage describes a two dimensional array of pixels to draw. The pixels may be + decoded in a raster bitmap, encoded in a SkPicture or compressed data stream, + or located in GPU memory as a GPU texture. + + SkImage cannot be modified after it is created. SkImage may allocate additional + storage as needed; for instance, an encoded SkImage may decode when drawn. + + SkImage width and height are greater than zero. Creating an SkImage with zero width + or height returns SkImage equal to nullptr. + + SkImage may be created from SkBitmap, SkPixmap, SkSurface, SkPicture, encoded streams, + GPU texture, YUV_ColorSpace data, or hardware buffer. Encoded streams supported + include BMP, GIF, HEIF, ICO, JPEG, PNG, WBMP, WebP. Supported encoding details + vary with platform. + + See SkImages namespace for the static factory methods to make SkImages. + + Clients should *not* subclass SkImage as there is a lot of internal machinery that is + not publicly accessible. +*/ +class SK_API SkImage : public SkRefCnt { +public: + /** Returns a SkImageInfo describing the width, height, color type, alpha type, and color space + of the SkImage. + + @return image info of SkImage. + */ + const SkImageInfo& imageInfo() const { return fInfo; } + + /** Returns pixel count in each row. + + @return pixel width in SkImage + */ + int width() const { return fInfo.width(); } + + /** Returns pixel row count. + + @return pixel height in SkImage + */ + int height() const { return fInfo.height(); } + + /** Returns SkISize { width(), height() }. + + @return integral size of width() and height() + */ + SkISize dimensions() const { return SkISize::Make(fInfo.width(), fInfo.height()); } + + /** Returns SkIRect { 0, 0, width(), height() }. + + @return integral rectangle from origin to width() and height() + */ + SkIRect bounds() const { return SkIRect::MakeWH(fInfo.width(), fInfo.height()); } + + /** Returns value unique to image. SkImage contents cannot change after SkImage is + created. Any operation to create a new SkImage will receive generate a new + unique number. + + @return unique identifier + */ + uint32_t uniqueID() const { return fUniqueID; } + + /** Returns SkAlphaType. + + SkAlphaType returned was a parameter to an SkImage constructor, + or was parsed from encoded data. + + @return SkAlphaType in SkImage + + example: https://fiddle.skia.org/c/@Image_alphaType + */ + SkAlphaType alphaType() const; + + /** Returns SkColorType if known; otherwise, returns kUnknown_SkColorType. + + @return SkColorType of SkImage + + example: https://fiddle.skia.org/c/@Image_colorType + */ + SkColorType colorType() const; + + /** Returns SkColorSpace, the range of colors, associated with SkImage. The + reference count of SkColorSpace is unchanged. The returned SkColorSpace is + immutable. + + SkColorSpace returned was passed to an SkImage constructor, + or was parsed from encoded data. SkColorSpace returned may be ignored when SkImage + is drawn, depending on the capabilities of the SkSurface receiving the drawing. + + @return SkColorSpace in SkImage, or nullptr + + example: https://fiddle.skia.org/c/@Image_colorSpace + */ + SkColorSpace* colorSpace() const; + + /** Returns a smart pointer to SkColorSpace, the range of colors, associated with + SkImage. The smart pointer tracks the number of objects sharing this + SkColorSpace reference so the memory is released when the owners destruct. + + The returned SkColorSpace is immutable. + + SkColorSpace returned was passed to an SkImage constructor, + or was parsed from encoded data. SkColorSpace returned may be ignored when SkImage + is drawn, depending on the capabilities of the SkSurface receiving the drawing. + + @return SkColorSpace in SkImage, or nullptr, wrapped in a smart pointer + + example: https://fiddle.skia.org/c/@Image_refColorSpace + */ + sk_sp refColorSpace() const; + + /** Returns true if SkImage pixels represent transparency only. If true, each pixel + is packed in 8 bits as defined by kAlpha_8_SkColorType. + + @return true if pixels represent a transparency mask + + example: https://fiddle.skia.org/c/@Image_isAlphaOnly + */ + bool isAlphaOnly() const; + + /** Returns true if pixels ignore their alpha value and are treated as fully opaque. + + @return true if SkAlphaType is kOpaque_SkAlphaType + */ + bool isOpaque() const { return SkAlphaTypeIsOpaque(this->alphaType()); } + + /** + * Make a shader with the specified tiling and mipmap sampling. + */ + sk_sp makeShader(SkTileMode tmx, SkTileMode tmy, const SkSamplingOptions&, + const SkMatrix* localMatrix = nullptr) const; + sk_sp makeShader(SkTileMode tmx, SkTileMode tmy, const SkSamplingOptions& sampling, + const SkMatrix& lm) const; + /** Defaults to clamp in both X and Y. */ + sk_sp makeShader(const SkSamplingOptions& sampling, const SkMatrix& lm) const; + sk_sp makeShader(const SkSamplingOptions& sampling, + const SkMatrix* lm = nullptr) const; + + /** + * makeRawShader functions like makeShader, but for images that contain non-color data. + * This includes images encoding things like normals, material properties (eg, roughness), + * heightmaps, or any other purely mathematical data that happens to be stored in an image. + * These types of images are useful with some programmable shaders (see: SkRuntimeEffect). + * + * Raw image shaders work like regular image shaders (including filtering and tiling), with + * a few major differences: + * - No color space transformation is ever applied (the color space of the image is ignored). + * - Images with an alpha type of kUnpremul are *not* automatically premultiplied. + * - Bicubic filtering is not supported. If SkSamplingOptions::useCubic is true, these + * factories will return nullptr. + */ + sk_sp makeRawShader(SkTileMode tmx, SkTileMode tmy, const SkSamplingOptions&, + const SkMatrix* localMatrix = nullptr) const; + sk_sp makeRawShader(SkTileMode tmx, SkTileMode tmy, const SkSamplingOptions& sampling, + const SkMatrix& lm) const; + /** Defaults to clamp in both X and Y. */ + sk_sp makeRawShader(const SkSamplingOptions& sampling, const SkMatrix& lm) const; + sk_sp makeRawShader(const SkSamplingOptions& sampling, + const SkMatrix* lm = nullptr) const; + + /** Copies SkImage pixel address, row bytes, and SkImageInfo to pixmap, if address + is available, and returns true. If pixel address is not available, return + false and leave pixmap unchanged. + + @param pixmap storage for pixel state if pixels are readable; otherwise, ignored + @return true if SkImage has direct access to pixels + + example: https://fiddle.skia.org/c/@Image_peekPixels + */ + bool peekPixels(SkPixmap* pixmap) const; + + /** Returns true if the contents of SkImage was created on or uploaded to GPU memory, + and is available as a GPU texture. + + @return true if SkImage is a GPU texture + + example: https://fiddle.skia.org/c/@Image_isTextureBacked + */ + virtual bool isTextureBacked() const = 0; + + /** Returns an approximation of the amount of texture memory used by the image. Returns + zero if the image is not texture backed or if the texture has an external format. + */ + virtual size_t textureSize() const = 0; + + /** Returns true if SkImage can be drawn on either raster surface or GPU surface. + If context is nullptr, tests if SkImage draws on raster surface; + otherwise, tests if SkImage draws on GPU surface associated with context. + + SkImage backed by GPU texture may become invalid if associated context is + invalid. lazy image may be invalid and may not draw to raster surface or + GPU surface or both. + + @param context GPU context + @return true if SkImage can be drawn + + example: https://fiddle.skia.org/c/@Image_isValid + */ + virtual bool isValid(GrRecordingContext* context) const = 0; + + /** \enum SkImage::CachingHint + CachingHint selects whether Skia may internally cache SkBitmap generated by + decoding SkImage, or by copying SkImage from GPU to CPU. The default behavior + allows caching SkBitmap. + + Choose kDisallow_CachingHint if SkImage pixels are to be used only once, or + if SkImage pixels reside in a cache outside of Skia, or to reduce memory pressure. + + Choosing kAllow_CachingHint does not ensure that pixels will be cached. + SkImage pixels may not be cached if memory requirements are too large or + pixels are not accessible. + */ + enum CachingHint { + kAllow_CachingHint, //!< allows internally caching decoded and copied pixels + kDisallow_CachingHint, //!< disallows internally caching decoded and copied pixels + }; + + /** Copies SkRect of pixels from SkImage to dstPixels. Copy starts at offset (srcX, srcY), + and does not exceed SkImage (width(), height()). + + dstInfo specifies width, height, SkColorType, SkAlphaType, and SkColorSpace of + destination. dstRowBytes specifies the gap from one destination row to the next. + Returns true if pixels are copied. Returns false if: + - dstInfo.addr() equals nullptr + - dstRowBytes is less than dstInfo.minRowBytes() + - SkPixelRef is nullptr + + Pixels are copied only if pixel conversion is possible. If SkImage SkColorType is + kGray_8_SkColorType, or kAlpha_8_SkColorType; dstInfo.colorType() must match. + If SkImage SkColorType is kGray_8_SkColorType, dstInfo.colorSpace() must match. + If SkImage SkAlphaType is kOpaque_SkAlphaType, dstInfo.alphaType() must + match. If SkImage SkColorSpace is nullptr, dstInfo.colorSpace() must match. Returns + false if pixel conversion is not possible. + + srcX and srcY may be negative to copy only top or left of source. Returns + false if width() or height() is zero or negative. + Returns false if abs(srcX) >= Image width(), or if abs(srcY) >= Image height(). + + If cachingHint is kAllow_CachingHint, pixels may be retained locally. + If cachingHint is kDisallow_CachingHint, pixels are not added to the local cache. + + @param context the GrDirectContext in play, if it exists + @param dstInfo destination width, height, SkColorType, SkAlphaType, SkColorSpace + @param dstPixels destination pixel storage + @param dstRowBytes destination row length + @param srcX column index whose absolute value is less than width() + @param srcY row index whose absolute value is less than height() + @param cachingHint whether the pixels should be cached locally + @return true if pixels are copied to dstPixels + */ + bool readPixels(GrDirectContext* context, + const SkImageInfo& dstInfo, + void* dstPixels, + size_t dstRowBytes, + int srcX, int srcY, + CachingHint cachingHint = kAllow_CachingHint) const; + + /** Copies a SkRect of pixels from SkImage to dst. Copy starts at (srcX, srcY), and + does not exceed SkImage (width(), height()). + + dst specifies width, height, SkColorType, SkAlphaType, SkColorSpace, pixel storage, + and row bytes of destination. dst.rowBytes() specifics the gap from one destination + row to the next. Returns true if pixels are copied. Returns false if: + - dst pixel storage equals nullptr + - dst.rowBytes is less than SkImageInfo::minRowBytes + - SkPixelRef is nullptr + + Pixels are copied only if pixel conversion is possible. If SkImage SkColorType is + kGray_8_SkColorType, or kAlpha_8_SkColorType; dst.colorType() must match. + If SkImage SkColorType is kGray_8_SkColorType, dst.colorSpace() must match. + If SkImage SkAlphaType is kOpaque_SkAlphaType, dst.alphaType() must + match. If SkImage SkColorSpace is nullptr, dst.colorSpace() must match. Returns + false if pixel conversion is not possible. + + srcX and srcY may be negative to copy only top or left of source. Returns + false if width() or height() is zero or negative. + Returns false if abs(srcX) >= Image width(), or if abs(srcY) >= Image height(). + + If cachingHint is kAllow_CachingHint, pixels may be retained locally. + If cachingHint is kDisallow_CachingHint, pixels are not added to the local cache. + + @param context the GrDirectContext in play, if it exists + @param dst destination SkPixmap: SkImageInfo, pixels, row bytes + @param srcX column index whose absolute value is less than width() + @param srcY row index whose absolute value is less than height() + @param cachingHint whether the pixels should be cached locallyZ + @return true if pixels are copied to dst + */ + bool readPixels(GrDirectContext* context, + const SkPixmap& dst, + int srcX, + int srcY, + CachingHint cachingHint = kAllow_CachingHint) const; + +#if defined(GRAPHITE_TEST_UTILS) + bool readPixelsGraphite(skgpu::graphite::Recorder*, + const SkPixmap& dst, + int srcX, + int srcY) const; +#endif + +#ifndef SK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API + /** Deprecated. Use the variants that accept a GrDirectContext. */ + bool readPixels(const SkImageInfo& dstInfo, void* dstPixels, size_t dstRowBytes, + int srcX, int srcY, CachingHint cachingHint = kAllow_CachingHint) const; + bool readPixels(const SkPixmap& dst, int srcX, int srcY, + CachingHint cachingHint = kAllow_CachingHint) const; +#endif + + /** The result from asyncRescaleAndReadPixels() or asyncRescaleAndReadPixelsYUV420(). */ + class AsyncReadResult { + public: + AsyncReadResult(const AsyncReadResult&) = delete; + AsyncReadResult(AsyncReadResult&&) = delete; + AsyncReadResult& operator=(const AsyncReadResult&) = delete; + AsyncReadResult& operator=(AsyncReadResult&&) = delete; + + virtual ~AsyncReadResult() = default; + virtual int count() const = 0; + virtual const void* data(int i) const = 0; + virtual size_t rowBytes(int i) const = 0; + + protected: + AsyncReadResult() = default; + }; + + /** Client-provided context that is passed to client-provided ReadPixelsContext. */ + using ReadPixelsContext = void*; + + /** Client-provided callback to asyncRescaleAndReadPixels() or + asyncRescaleAndReadPixelsYUV420() that is called when read result is ready or on failure. + */ + using ReadPixelsCallback = void(ReadPixelsContext, std::unique_ptr); + + enum class RescaleGamma : bool { kSrc, kLinear }; + + enum class RescaleMode { + kNearest, + kLinear, + kRepeatedLinear, + kRepeatedCubic, + }; + + /** Makes image pixel data available to caller, possibly asynchronously. It can also rescale + the image pixels. + + Currently asynchronous reads are only supported on the GPU backend and only when the + underlying 3D API supports transfer buffers and CPU/GPU synchronization primitives. In all + other cases this operates synchronously. + + Data is read from the source sub-rectangle, is optionally converted to a linear gamma, is + rescaled to the size indicated by 'info', is then converted to the color space, color type, + and alpha type of 'info'. A 'srcRect' that is not contained by the bounds of the image + causes failure. + + When the pixel data is ready the caller's ReadPixelsCallback is called with a + AsyncReadResult containing pixel data in the requested color type, alpha type, and color + space. The AsyncReadResult will have count() == 1. Upon failure the callback is called with + nullptr for AsyncReadResult. For a GPU image this flushes work but a submit must occur to + guarantee a finite time before the callback is called. + + The data is valid for the lifetime of AsyncReadResult with the exception that if the SkImage + is GPU-backed the data is immediately invalidated if the context is abandoned or + destroyed. + + @param info info of the requested pixels + @param srcRect subrectangle of image to read + @param rescaleGamma controls whether rescaling is done in the image's gamma or whether + the source data is transformed to a linear gamma before rescaling. + @param rescaleMode controls the technique (and cost) of the rescaling + @param callback function to call with result of the read + @param context passed to callback + */ + void asyncRescaleAndReadPixels(const SkImageInfo& info, + const SkIRect& srcRect, + RescaleGamma rescaleGamma, + RescaleMode rescaleMode, + ReadPixelsCallback callback, + ReadPixelsContext context) const; + + /** + Similar to asyncRescaleAndReadPixels but performs an additional conversion to YUV. The + RGB->YUV conversion is controlled by 'yuvColorSpace'. The YUV data is returned as three + planes ordered y, u, v. The u and v planes are half the width and height of the resized + rectangle. The y, u, and v values are single bytes. Currently this fails if 'dstSize' + width and height are not even. A 'srcRect' that is not contained by the bounds of the + image causes failure. + + When the pixel data is ready the caller's ReadPixelsCallback is called with a + AsyncReadResult containing the planar data. The AsyncReadResult will have count() == 3. + Upon failure the callback is called with nullptr for AsyncReadResult. For a GPU image this + flushes work but a submit must occur to guarantee a finite time before the callback is + called. + + The data is valid for the lifetime of AsyncReadResult with the exception that if the SkImage + is GPU-backed the data is immediately invalidated if the context is abandoned or + destroyed. + + @param yuvColorSpace The transformation from RGB to YUV. Applied to the resized image + after it is converted to dstColorSpace. + @param dstColorSpace The color space to convert the resized image to, after rescaling. + @param srcRect The portion of the image to rescale and convert to YUV planes. + @param dstSize The size to rescale srcRect to + @param rescaleGamma controls whether rescaling is done in the image's gamma or whether + the source data is transformed to a linear gamma before rescaling. + @param rescaleMode controls the technique (and cost) of the rescaling + @param callback function to call with the planar read result + @param context passed to callback + */ + void asyncRescaleAndReadPixelsYUV420(SkYUVColorSpace yuvColorSpace, + sk_sp dstColorSpace, + const SkIRect& srcRect, + const SkISize& dstSize, + RescaleGamma rescaleGamma, + RescaleMode rescaleMode, + ReadPixelsCallback callback, + ReadPixelsContext context) const; + + /** + * Identical to asyncRescaleAndReadPixelsYUV420 but a fourth plane is returned in the + * AsyncReadResult passed to 'callback'. The fourth plane contains the alpha chanel at the + * same full resolution as the Y plane. + */ + void asyncRescaleAndReadPixelsYUVA420(SkYUVColorSpace yuvColorSpace, + sk_sp dstColorSpace, + const SkIRect& srcRect, + const SkISize& dstSize, + RescaleGamma rescaleGamma, + RescaleMode rescaleMode, + ReadPixelsCallback callback, + ReadPixelsContext context) const; + + /** Copies SkImage to dst, scaling pixels to fit dst.width() and dst.height(), and + converting pixels to match dst.colorType() and dst.alphaType(). Returns true if + pixels are copied. Returns false if dst.addr() is nullptr, or dst.rowBytes() is + less than dst SkImageInfo::minRowBytes. + + Pixels are copied only if pixel conversion is possible. If SkImage SkColorType is + kGray_8_SkColorType, or kAlpha_8_SkColorType; dst.colorType() must match. + If SkImage SkColorType is kGray_8_SkColorType, dst.colorSpace() must match. + If SkImage SkAlphaType is kOpaque_SkAlphaType, dst.alphaType() must + match. If SkImage SkColorSpace is nullptr, dst.colorSpace() must match. Returns + false if pixel conversion is not possible. + + If cachingHint is kAllow_CachingHint, pixels may be retained locally. + If cachingHint is kDisallow_CachingHint, pixels are not added to the local cache. + + @param dst destination SkPixmap: SkImageInfo, pixels, row bytes + @return true if pixels are scaled to fit dst + */ + bool scalePixels(const SkPixmap& dst, const SkSamplingOptions&, + CachingHint cachingHint = kAllow_CachingHint) const; + + /** Returns encoded SkImage pixels as SkData, if SkImage was created from supported + encoded stream format. Platform support for formats vary and may require building + with one or more of: SK_ENCODE_JPEG, SK_ENCODE_PNG, SK_ENCODE_WEBP. + + Returns nullptr if SkImage contents are not encoded. + + @return encoded SkImage, or nullptr + + example: https://fiddle.skia.org/c/@Image_refEncodedData + */ + sk_sp refEncodedData() const; + + /** Returns subset of this image. + + Returns nullptr if any of the following are true: + - Subset is empty + - Subset is not contained inside the image's bounds + - Pixels in the source image could not be read or copied + - This image is texture-backed and the provided context is null or does not match + the source image's context. + + If the source image was texture-backed, the resulting image will be texture-backed also. + Otherwise, the returned image will be raster-backed. + + @param direct the GrDirectContext of the source image (nullptr is ok if the source image + is not texture-backed). + @param subset bounds of returned SkImage + @return the subsetted image, or nullptr + + example: https://fiddle.skia.org/c/@Image_makeSubset + */ + virtual sk_sp makeSubset(GrDirectContext* direct, const SkIRect& subset) const = 0; + + struct RequiredProperties { + bool fMipmapped; + + bool operator==(const RequiredProperties& other) const { + return fMipmapped == other.fMipmapped; + } + + bool operator!=(const RequiredProperties& other) const { return !(*this == other); } + + bool operator<(const RequiredProperties& other) const { + return fMipmapped < other.fMipmapped; + } + }; + + /** Returns subset of this image. + + Returns nullptr if any of the following are true: + - Subset is empty + - Subset is not contained inside the image's bounds + - Pixels in the image could not be read or copied + - This image is texture-backed and the provided context is null or does not match + the source image's context. + + If the source image was texture-backed, the resulting image will be texture-backed also. + Otherwise, the returned image will be raster-backed. + + @param recorder the recorder of the source image (nullptr is ok if the + source image was texture-backed). + @param subset bounds of returned SkImage + @param RequiredProperties properties the returned SkImage must possess (e.g. mipmaps) + @return the subsetted image, or nullptr + */ + virtual sk_sp makeSubset(skgpu::graphite::Recorder*, + const SkIRect& subset, + RequiredProperties) const = 0; + + /** + * Returns true if the image has mipmap levels. + */ + bool hasMipmaps() const; + + /** + * Returns true if the image holds protected content. + */ + bool isProtected() const; + + /** + * Returns an image with the same "base" pixels as the this image, but with mipmap levels + * automatically generated and attached. + */ + sk_sp withDefaultMipmaps() const; + + /** Returns raster image or lazy image. Copies SkImage backed by GPU texture into + CPU memory if needed. Returns original SkImage if decoded in raster bitmap, + or if encoded in a stream. + + Returns nullptr if backed by GPU texture and copy fails. + + @return raster image, lazy image, or nullptr + + example: https://fiddle.skia.org/c/@Image_makeNonTextureImage + */ + sk_sp makeNonTextureImage(GrDirectContext* = nullptr) const; + + /** Returns raster image. Copies SkImage backed by GPU texture into CPU memory, + or decodes SkImage from lazy image. Returns original SkImage if decoded in + raster bitmap. + + Returns nullptr if copy, decode, or pixel read fails. + + If cachingHint is kAllow_CachingHint, pixels may be retained locally. + If cachingHint is kDisallow_CachingHint, pixels are not added to the local cache. + + @return raster image, or nullptr + + example: https://fiddle.skia.org/c/@Image_makeRasterImage + */ + sk_sp makeRasterImage(GrDirectContext*, + CachingHint cachingHint = kDisallow_CachingHint) const; + +#if !defined(SK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API) + sk_sp makeRasterImage(CachingHint cachingHint = kDisallow_CachingHint) const { + return this->makeRasterImage(nullptr, cachingHint); + } +#endif + + /** Deprecated. + */ + enum LegacyBitmapMode { + kRO_LegacyBitmapMode, //!< returned bitmap is read-only and immutable + }; + + /** Deprecated. + Creates raster SkBitmap with same pixels as SkImage. If legacyBitmapMode is + kRO_LegacyBitmapMode, returned bitmap is read-only and immutable. + Returns true if SkBitmap is stored in bitmap. Returns false and resets bitmap if + SkBitmap write did not succeed. + + @param bitmap storage for legacy SkBitmap + @param legacyBitmapMode bitmap is read-only and immutable + @return true if SkBitmap was created + */ + bool asLegacyBitmap(SkBitmap* bitmap, + LegacyBitmapMode legacyBitmapMode = kRO_LegacyBitmapMode) const; + + /** Returns true if SkImage is backed by an image-generator or other service that creates + and caches its pixels or texture on-demand. + + @return true if SkImage is created as needed + + example: https://fiddle.skia.org/c/@Image_isLazyGenerated_a + example: https://fiddle.skia.org/c/@Image_isLazyGenerated_b + */ + virtual bool isLazyGenerated() const = 0; + + /** Creates SkImage in target SkColorSpace. + Returns nullptr if SkImage could not be created. + + Returns original SkImage if it is in target SkColorSpace. + Otherwise, converts pixels from SkImage SkColorSpace to target SkColorSpace. + If SkImage colorSpace() returns nullptr, SkImage SkColorSpace is assumed to be sRGB. + + If this image is texture-backed, the context parameter is required and must match the + context of the source image. + + @param direct The GrDirectContext in play, if it exists + @param target SkColorSpace describing color range of returned SkImage + @return created SkImage in target SkColorSpace + + example: https://fiddle.skia.org/c/@Image_makeColorSpace + */ + virtual sk_sp makeColorSpace(GrDirectContext* direct, + sk_sp target) const = 0; + + /** Creates SkImage in target SkColorSpace. + Returns nullptr if SkImage could not be created. + + Returns original SkImage if it is in target SkColorSpace. + Otherwise, converts pixels from SkImage SkColorSpace to target SkColorSpace. + If SkImage colorSpace() returns nullptr, SkImage SkColorSpace is assumed to be sRGB. + + If this image is graphite-backed, the recorder parameter is required. + + @param targetColorSpace SkColorSpace describing color range of returned SkImage + @param recorder The Recorder in which to create the new image + @param RequiredProperties properties the returned SkImage must possess (e.g. mipmaps) + @return created SkImage in target SkColorSpace + */ + virtual sk_sp makeColorSpace(skgpu::graphite::Recorder*, + sk_sp targetColorSpace, + RequiredProperties) const = 0; + + /** Experimental. + Creates SkImage in target SkColorType and SkColorSpace. + Returns nullptr if SkImage could not be created. + + Returns original SkImage if it is in target SkColorType and SkColorSpace. + + If this image is texture-backed, the context parameter is required and must match the + context of the source image. + + @param direct The GrDirectContext in play, if it exists + @param targetColorType SkColorType of returned SkImage + @param targetColorSpace SkColorSpace of returned SkImage + @return created SkImage in target SkColorType and SkColorSpace + */ + virtual sk_sp makeColorTypeAndColorSpace(GrDirectContext* direct, + SkColorType targetColorType, + sk_sp targetCS) const = 0; + + /** Experimental. + Creates SkImage in target SkColorType and SkColorSpace. + Returns nullptr if SkImage could not be created. + + Returns original SkImage if it is in target SkColorType and SkColorSpace. + + If this image is graphite-backed, the recorder parameter is required. + + @param targetColorType SkColorType of returned SkImage + @param targetColorSpace SkColorSpace of returned SkImage + @param recorder The Recorder in which to create the new image + @param RequiredProperties properties the returned SkImage must possess (e.g. mipmaps) + @return created SkImage in target SkColorType and SkColorSpace + */ + virtual sk_sp makeColorTypeAndColorSpace(skgpu::graphite::Recorder*, + SkColorType targetColorType, + sk_sp targetColorSpace, + RequiredProperties) const = 0; + + /** Creates a new SkImage identical to this one, but with a different SkColorSpace. + This does not convert the underlying pixel data, so the resulting image will draw + differently. + */ + sk_sp reinterpretColorSpace(sk_sp newColorSpace) const; + +private: + SkImage(const SkImageInfo& info, uint32_t uniqueID); + + friend class SkBitmap; + friend class SkImage_Base; // for private ctor + friend class SkImage_Raster; // for withMipmaps + friend class SkMipmapBuilder; + + SkImageInfo fInfo; + const uint32_t fUniqueID; + + sk_sp withMipmaps(sk_sp) const; + + using INHERITED = SkRefCnt; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkImageFilter.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkImageFilter.h new file mode 100644 index 00000000000..7352cf71f72 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkImageFilter.h @@ -0,0 +1,119 @@ +/* + * Copyright 2011 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkImageFilter_DEFINED +#define SkImageFilter_DEFINED + +#include "include/core/SkFlattenable.h" +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +#include + +class SkColorFilter; +class SkMatrix; +struct SkDeserialProcs; + +/** + * Base class for image filters. If one is installed in the paint, then all drawing occurs as + * usual, but it is as if the drawing happened into an offscreen (before the xfermode is applied). + * This offscreen bitmap will then be handed to the imagefilter, who in turn creates a new bitmap + * which is what will finally be drawn to the device (using the original xfermode). + * + * The local space of image filters matches the local space of the drawn geometry. For instance if + * there is rotation on the canvas, the blur will be computed along those rotated axes and not in + * the device space. In order to achieve this result, the actual drawing of the geometry may happen + * in an unrotated coordinate system so that the filtered image can be computed more easily, and + * then it will be post transformed to match what would have been produced if the geometry were + * drawn with the total canvas matrix to begin with. + */ +class SK_API SkImageFilter : public SkFlattenable { +public: + enum MapDirection { + kForward_MapDirection, + kReverse_MapDirection, + }; + /** + * Map a device-space rect recursively forward or backward through the filter DAG. + * kForward_MapDirection is used to determine which pixels of the destination canvas a source + * image rect would touch after filtering. kReverse_MapDirection is used to determine which rect + * of the source image would be required to fill the given rect (typically, clip bounds). Used + * for clipping and temp-buffer allocations, so the result need not be exact, but should never + * be smaller than the real answer. The default implementation recursively unions all input + * bounds, or returns the source rect if no inputs. + * + * In kReverse mode, 'inputRect' is the device-space bounds of the input pixels. In kForward + * mode it should always be null. If 'inputRect' is null in kReverse mode the resulting answer + * may be incorrect. + */ + SkIRect filterBounds(const SkIRect& src, const SkMatrix& ctm, + MapDirection, const SkIRect* inputRect = nullptr) const; + + /** + * Returns whether this image filter is a color filter and puts the color filter into the + * "filterPtr" parameter if it can. Does nothing otherwise. + * If this returns false, then the filterPtr is unchanged. + * If this returns true, then if filterPtr is not null, it must be set to a ref'd colorfitler + * (i.e. it may not be set to NULL). + */ + bool isColorFilterNode(SkColorFilter** filterPtr) const; + + // DEPRECATED : use isColorFilterNode() instead + bool asColorFilter(SkColorFilter** filterPtr) const { + return this->isColorFilterNode(filterPtr); + } + + /** + * Returns true (and optionally returns a ref'd filter) if this imagefilter can be completely + * replaced by the returned colorfilter. i.e. the two effects will affect drawing in the same + * way. + */ + bool asAColorFilter(SkColorFilter** filterPtr) const; + + /** + * Returns the number of inputs this filter will accept (some inputs can be NULL). + */ + int countInputs() const; + + /** + * Returns the input filter at a given index, or NULL if no input is connected. The indices + * used are filter-specific. + */ + const SkImageFilter* getInput(int i) const; + + // Default impl returns union of all input bounds. + virtual SkRect computeFastBounds(const SkRect& bounds) const; + + // Can this filter DAG compute the resulting bounds of an object-space rectangle? + bool canComputeFastBounds() const; + + /** + * If this filter can be represented by another filter + a localMatrix, return that filter, + * else return null. + */ + sk_sp makeWithLocalMatrix(const SkMatrix& matrix) const; + + static sk_sp Deserialize(const void* data, size_t size, + const SkDeserialProcs* procs = nullptr) { + return sk_sp(static_cast( + SkFlattenable::Deserialize(kSkImageFilter_Type, data, size, procs).release())); + } + +protected: + + sk_sp refMe() const { + return sk_ref_sp(const_cast(this)); + } + +private: + friend class SkImageFilter_Base; + + using INHERITED = SkFlattenable; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkImageGenerator.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkImageGenerator.h new file mode 100644 index 00000000000..4210cdb6019 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkImageGenerator.h @@ -0,0 +1,148 @@ +/* + * Copyright 2013 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkImageGenerator_DEFINED +#define SkImageGenerator_DEFINED + +#include "include/core/SkData.h" +#include "include/core/SkImageInfo.h" +#include "include/core/SkPixmap.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkYUVAPixmaps.h" +#include "include/private/base/SkAPI.h" + +#if defined(SK_GRAPHITE) +#include "include/core/SkImage.h" +#include "include/gpu/graphite/Recorder.h" +#endif + +#include +#include + +class GrRecordingContext; + +class SK_API SkImageGenerator { +public: + /** + * The PixelRef which takes ownership of this SkImageGenerator + * will call the image generator's destructor. + */ + virtual ~SkImageGenerator() { } + + uint32_t uniqueID() const { return fUniqueID; } + + /** + * Return a ref to the encoded (i.e. compressed) representation + * of this data. + * + * If non-NULL is returned, the caller is responsible for calling + * unref() on the data when it is finished. + */ + sk_sp refEncodedData() { + return this->onRefEncodedData(); + } + + /** + * Return the ImageInfo associated with this generator. + */ + const SkImageInfo& getInfo() const { return fInfo; } + + /** + * Can this generator be used to produce images that will be drawable to the specified context + * (or to CPU, if context is nullptr)? + */ + bool isValid(GrRecordingContext* context) const { + return this->onIsValid(context); + } + + /** + * Will this generator produce protected content + */ + bool isProtected() const { + return this->onIsProtected(); + } + + /** + * Decode into the given pixels, a block of memory of size at + * least (info.fHeight - 1) * rowBytes + (info.fWidth * + * bytesPerPixel) + * + * Repeated calls to this function should give the same results, + * allowing the PixelRef to be immutable. + * + * @param info A description of the format + * expected by the caller. This can simply be identical + * to the info returned by getInfo(). + * + * This contract also allows the caller to specify + * different output-configs, which the implementation can + * decide to support or not. + * + * A size that does not match getInfo() implies a request + * to scale. If the generator cannot perform this scale, + * it will return false. + * + * @return true on success. + */ + bool getPixels(const SkImageInfo& info, void* pixels, size_t rowBytes); + + bool getPixels(const SkPixmap& pm) { + return this->getPixels(pm.info(), pm.writable_addr(), pm.rowBytes()); + } + + /** + * If decoding to YUV is supported, this returns true. Otherwise, this + * returns false and the caller will ignore output parameter yuvaPixmapInfo. + * + * @param supportedDataTypes Indicates the data type/planar config combinations that are + * supported by the caller. If the generator supports decoding to + * YUV(A), but not as a type in supportedDataTypes, this method + * returns false. + * @param yuvaPixmapInfo Output parameter that specifies the planar configuration, subsampling, + * orientation, chroma siting, plane color types, and row bytes. + */ + bool queryYUVAInfo(const SkYUVAPixmapInfo::SupportedDataTypes& supportedDataTypes, + SkYUVAPixmapInfo* yuvaPixmapInfo) const; + + /** + * Returns true on success and false on failure. + * This always attempts to perform a full decode. To get the planar + * configuration without decoding use queryYUVAInfo(). + * + * @param yuvaPixmaps Contains preallocated pixmaps configured according to a successful call + * to queryYUVAInfo(). + */ + bool getYUVAPlanes(const SkYUVAPixmaps& yuvaPixmaps); + + virtual bool isTextureGenerator() const { return false; } + +protected: + static constexpr int kNeedNewImageUniqueID = 0; + + SkImageGenerator(const SkImageInfo& info, uint32_t uniqueId = kNeedNewImageUniqueID); + + virtual sk_sp onRefEncodedData() { return nullptr; } + struct Options {}; + virtual bool onGetPixels(const SkImageInfo&, void*, size_t, const Options&) { return false; } + virtual bool onIsValid(GrRecordingContext*) const { return true; } + virtual bool onIsProtected() const { return false; } + virtual bool onQueryYUVAInfo(const SkYUVAPixmapInfo::SupportedDataTypes&, + SkYUVAPixmapInfo*) const { return false; } + virtual bool onGetYUVAPlanes(const SkYUVAPixmaps&) { return false; } + + const SkImageInfo fInfo; + +private: + const uint32_t fUniqueID; + + SkImageGenerator(SkImageGenerator&&) = delete; + SkImageGenerator(const SkImageGenerator&) = delete; + SkImageGenerator& operator=(SkImageGenerator&&) = delete; + SkImageGenerator& operator=(const SkImageGenerator&) = delete; +}; + +#endif // SkImageGenerator_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkImageInfo.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkImageInfo.h new file mode 100644 index 00000000000..4d08e429fc8 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkImageInfo.h @@ -0,0 +1,628 @@ +/* + * Copyright 2013 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkImageInfo_DEFINED +#define SkImageInfo_DEFINED + +#include "include/core/SkAlphaType.h" +#include "include/core/SkColorType.h" +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkSize.h" +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkDebug.h" +#include "include/private/base/SkMath.h" +#include "include/private/base/SkTFitsIn.h" + +#include +#include +#include + +class SkColorSpace; + +/** Returns the number of bytes required to store a pixel, including unused padding. + Returns zero if ct is kUnknown_SkColorType or invalid. + + @return bytes per pixel +*/ +SK_API int SkColorTypeBytesPerPixel(SkColorType ct); + +/** Returns true if SkColorType always decodes alpha to 1.0, making the pixel + fully opaque. If true, SkColorType does not reserve bits to encode alpha. + + @return true if alpha is always set to 1.0 +*/ +SK_API bool SkColorTypeIsAlwaysOpaque(SkColorType ct); + +/** Returns true if canonical can be set to a valid SkAlphaType for colorType. If + there is more than one valid canonical SkAlphaType, set to alphaType, if valid. + If true is returned and canonical is not nullptr, store valid SkAlphaType. + + Returns false only if alphaType is kUnknown_SkAlphaType, color type is not + kUnknown_SkColorType, and SkColorType is not always opaque. If false is returned, + canonical is ignored. + + @param canonical storage for SkAlphaType + @return true if valid SkAlphaType can be associated with colorType +*/ +SK_API bool SkColorTypeValidateAlphaType(SkColorType colorType, SkAlphaType alphaType, + SkAlphaType* canonical = nullptr); + +/** \enum SkImageInfo::SkYUVColorSpace + Describes color range of YUV pixels. The color mapping from YUV to RGB varies + depending on the source. YUV pixels may be generated by JPEG images, standard + video streams, or high definition video streams. Each has its own mapping from + YUV to RGB. + + JPEG YUV values encode the full range of 0 to 255 for all three components. + Video YUV values often range from 16 to 235 for Y and from 16 to 240 for U and V (limited). + Details of encoding and conversion to RGB are described in YCbCr color space. + + The identity colorspace exists to provide a utility mapping from Y to R, U to G and V to B. + It can be used to visualize the YUV planes or to explicitly post process the YUV channels. +*/ +enum SkYUVColorSpace : int { + kJPEG_Full_SkYUVColorSpace, //!< describes full range + kRec601_Limited_SkYUVColorSpace, //!< describes SDTV range + kRec709_Full_SkYUVColorSpace, //!< describes HDTV range + kRec709_Limited_SkYUVColorSpace, + kBT2020_8bit_Full_SkYUVColorSpace, //!< describes UHDTV range, non-constant-luminance + kBT2020_8bit_Limited_SkYUVColorSpace, + kBT2020_10bit_Full_SkYUVColorSpace, + kBT2020_10bit_Limited_SkYUVColorSpace, + kBT2020_12bit_Full_SkYUVColorSpace, + kBT2020_12bit_Limited_SkYUVColorSpace, + kFCC_Full_SkYUVColorSpace, //!< describes FCC range + kFCC_Limited_SkYUVColorSpace, + kSMPTE240_Full_SkYUVColorSpace, //!< describes SMPTE240M range + kSMPTE240_Limited_SkYUVColorSpace, + kYDZDX_Full_SkYUVColorSpace, //!< describes YDZDX range + kYDZDX_Limited_SkYUVColorSpace, + kGBR_Full_SkYUVColorSpace, //!< describes GBR range + kGBR_Limited_SkYUVColorSpace, + kYCgCo_8bit_Full_SkYUVColorSpace, //!< describes YCgCo matrix + kYCgCo_8bit_Limited_SkYUVColorSpace, + kYCgCo_10bit_Full_SkYUVColorSpace, + kYCgCo_10bit_Limited_SkYUVColorSpace, + kYCgCo_12bit_Full_SkYUVColorSpace, + kYCgCo_12bit_Limited_SkYUVColorSpace, + kIdentity_SkYUVColorSpace, //!< maps Y->R, U->G, V->B + + kLastEnum_SkYUVColorSpace = kIdentity_SkYUVColorSpace, //!< last valid value + + // Legacy (deprecated) names: + kJPEG_SkYUVColorSpace = kJPEG_Full_SkYUVColorSpace, + kRec601_SkYUVColorSpace = kRec601_Limited_SkYUVColorSpace, + kRec709_SkYUVColorSpace = kRec709_Limited_SkYUVColorSpace, + kBT2020_SkYUVColorSpace = kBT2020_8bit_Limited_SkYUVColorSpace, +}; + +/** \struct SkColorInfo + Describes pixel and encoding. SkImageInfo can be created from SkColorInfo by + providing dimensions. + + It encodes how pixel bits describe alpha, transparency; color components red, blue, + and green; and SkColorSpace, the range and linearity of colors. +*/ +class SK_API SkColorInfo { +public: + /** Creates an SkColorInfo with kUnknown_SkColorType, kUnknown_SkAlphaType, + and no SkColorSpace. + + @return empty SkImageInfo + */ + SkColorInfo(); + ~SkColorInfo(); + + /** Creates SkColorInfo from SkColorType ct, SkAlphaType at, and optionally SkColorSpace cs. + + If SkColorSpace cs is nullptr and SkColorInfo is part of drawing source: SkColorSpace + defaults to sRGB, mapping into SkSurface SkColorSpace. + + Parameters are not validated to see if their values are legal, or that the + combination is supported. + @return created SkColorInfo + */ + SkColorInfo(SkColorType ct, SkAlphaType at, sk_sp cs); + + SkColorInfo(const SkColorInfo&); + SkColorInfo(SkColorInfo&&); + + SkColorInfo& operator=(const SkColorInfo&); + SkColorInfo& operator=(SkColorInfo&&); + + SkColorSpace* colorSpace() const; + sk_sp refColorSpace() const; + SkColorType colorType() const { return fColorType; } + SkAlphaType alphaType() const { return fAlphaType; } + + bool isOpaque() const { + return SkAlphaTypeIsOpaque(fAlphaType) + || SkColorTypeIsAlwaysOpaque(fColorType); + } + + bool gammaCloseToSRGB() const; + + /** Does other represent the same color type, alpha type, and color space? */ + bool operator==(const SkColorInfo& other) const; + + /** Does other represent a different color type, alpha type, or color space? */ + bool operator!=(const SkColorInfo& other) const; + + /** Creates SkColorInfo with same SkColorType, SkColorSpace, with SkAlphaType set + to newAlphaType. + + Created SkColorInfo contains newAlphaType even if it is incompatible with + SkColorType, in which case SkAlphaType in SkColorInfo is ignored. + */ + SkColorInfo makeAlphaType(SkAlphaType newAlphaType) const; + + /** Creates new SkColorInfo with same SkAlphaType, SkColorSpace, with SkColorType + set to newColorType. + */ + SkColorInfo makeColorType(SkColorType newColorType) const; + + /** Creates SkColorInfo with same SkAlphaType, SkColorType, with SkColorSpace + set to cs. cs may be nullptr. + */ + SkColorInfo makeColorSpace(sk_sp cs) const; + + /** Returns number of bytes per pixel required by SkColorType. + Returns zero if colorType() is kUnknown_SkColorType. + + @return bytes in pixel + + example: https://fiddle.skia.org/c/@ImageInfo_bytesPerPixel + */ + int bytesPerPixel() const; + + /** Returns bit shift converting row bytes to row pixels. + Returns zero for kUnknown_SkColorType. + + @return one of: 0, 1, 2, 3, 4; left shift to convert pixels to bytes + + example: https://fiddle.skia.org/c/@ImageInfo_shiftPerPixel + */ + int shiftPerPixel() const; + +private: + sk_sp fColorSpace; + SkColorType fColorType = kUnknown_SkColorType; + SkAlphaType fAlphaType = kUnknown_SkAlphaType; +}; + +/** \struct SkImageInfo + Describes pixel dimensions and encoding. SkBitmap, SkImage, PixMap, and SkSurface + can be created from SkImageInfo. SkImageInfo can be retrieved from SkBitmap and + SkPixmap, but not from SkImage and SkSurface. For example, SkImage and SkSurface + implementations may defer pixel depth, so may not completely specify SkImageInfo. + + SkImageInfo contains dimensions, the pixel integral width and height. It encodes + how pixel bits describe alpha, transparency; color components red, blue, + and green; and SkColorSpace, the range and linearity of colors. +*/ +struct SK_API SkImageInfo { +public: + + /** Creates an empty SkImageInfo with kUnknown_SkColorType, kUnknown_SkAlphaType, + a width and height of zero, and no SkColorSpace. + + @return empty SkImageInfo + */ + SkImageInfo() = default; + + /** Creates SkImageInfo from integral dimensions width and height, SkColorType ct, + SkAlphaType at, and optionally SkColorSpace cs. + + If SkColorSpace cs is nullptr and SkImageInfo is part of drawing source: SkColorSpace + defaults to sRGB, mapping into SkSurface SkColorSpace. + + Parameters are not validated to see if their values are legal, or that the + combination is supported. + + @param width pixel column count; must be zero or greater + @param height pixel row count; must be zero or greater + @param cs range of colors; may be nullptr + @return created SkImageInfo + */ + static SkImageInfo Make(int width, int height, SkColorType ct, SkAlphaType at); + static SkImageInfo Make(int width, int height, SkColorType ct, SkAlphaType at, + sk_sp cs); + static SkImageInfo Make(SkISize dimensions, SkColorType ct, SkAlphaType at); + static SkImageInfo Make(SkISize dimensions, SkColorType ct, SkAlphaType at, + sk_sp cs); + + /** Creates SkImageInfo from integral dimensions and SkColorInfo colorInfo, + + Parameters are not validated to see if their values are legal, or that the + combination is supported. + + @param dimensions pixel column and row count; must be zeros or greater + @param SkColorInfo the pixel encoding consisting of SkColorType, SkAlphaType, and + SkColorSpace (which may be nullptr) + @return created SkImageInfo + */ + static SkImageInfo Make(SkISize dimensions, const SkColorInfo& colorInfo) { + return SkImageInfo(dimensions, colorInfo); + } + static SkImageInfo Make(SkISize dimensions, SkColorInfo&& colorInfo) { + return SkImageInfo(dimensions, std::move(colorInfo)); + } + + /** Creates SkImageInfo from integral dimensions width and height, kN32_SkColorType, + SkAlphaType at, and optionally SkColorSpace cs. kN32_SkColorType will equal either + kBGRA_8888_SkColorType or kRGBA_8888_SkColorType, whichever is optimal. + + If SkColorSpace cs is nullptr and SkImageInfo is part of drawing source: SkColorSpace + defaults to sRGB, mapping into SkSurface SkColorSpace. + + Parameters are not validated to see if their values are legal, or that the + combination is supported. + + @param width pixel column count; must be zero or greater + @param height pixel row count; must be zero or greater + @param cs range of colors; may be nullptr + @return created SkImageInfo + */ + static SkImageInfo MakeN32(int width, int height, SkAlphaType at); + static SkImageInfo MakeN32(int width, int height, SkAlphaType at, sk_sp cs); + + /** Creates SkImageInfo from integral dimensions width and height, kN32_SkColorType, + SkAlphaType at, with sRGB SkColorSpace. + + Parameters are not validated to see if their values are legal, or that the + combination is supported. + + @param width pixel column count; must be zero or greater + @param height pixel row count; must be zero or greater + @return created SkImageInfo + + example: https://fiddle.skia.org/c/@ImageInfo_MakeS32 + */ + static SkImageInfo MakeS32(int width, int height, SkAlphaType at); + + /** Creates SkImageInfo from integral dimensions width and height, kN32_SkColorType, + kPremul_SkAlphaType, with optional SkColorSpace. + + If SkColorSpace cs is nullptr and SkImageInfo is part of drawing source: SkColorSpace + defaults to sRGB, mapping into SkSurface SkColorSpace. + + Parameters are not validated to see if their values are legal, or that the + combination is supported. + + @param width pixel column count; must be zero or greater + @param height pixel row count; must be zero or greater + @param cs range of colors; may be nullptr + @return created SkImageInfo + */ + static SkImageInfo MakeN32Premul(int width, int height); + static SkImageInfo MakeN32Premul(int width, int height, sk_sp cs); + + /** Creates SkImageInfo from integral dimensions width and height, kN32_SkColorType, + kPremul_SkAlphaType, with SkColorSpace set to nullptr. + + If SkImageInfo is part of drawing source: SkColorSpace defaults to sRGB, mapping + into SkSurface SkColorSpace. + + Parameters are not validated to see if their values are legal, or that the + combination is supported. + + @param dimensions width and height, each must be zero or greater + @param cs range of colors; may be nullptr + @return created SkImageInfo + */ + static SkImageInfo MakeN32Premul(SkISize dimensions); + static SkImageInfo MakeN32Premul(SkISize dimensions, sk_sp cs); + + /** Creates SkImageInfo from integral dimensions width and height, kAlpha_8_SkColorType, + kPremul_SkAlphaType, with SkColorSpace set to nullptr. + + @param width pixel column count; must be zero or greater + @param height pixel row count; must be zero or greater + @return created SkImageInfo + */ + static SkImageInfo MakeA8(int width, int height); + /** Creates SkImageInfo from integral dimensions, kAlpha_8_SkColorType, + kPremul_SkAlphaType, with SkColorSpace set to nullptr. + + @param dimensions pixel row and column count; must be zero or greater + @return created SkImageInfo + */ + static SkImageInfo MakeA8(SkISize dimensions); + + /** Creates SkImageInfo from integral dimensions width and height, kUnknown_SkColorType, + kUnknown_SkAlphaType, with SkColorSpace set to nullptr. + + Returned SkImageInfo as part of source does not draw, and as part of destination + can not be drawn to. + + @param width pixel column count; must be zero or greater + @param height pixel row count; must be zero or greater + @return created SkImageInfo + */ + static SkImageInfo MakeUnknown(int width, int height); + + /** Creates SkImageInfo from integral dimensions width and height set to zero, + kUnknown_SkColorType, kUnknown_SkAlphaType, with SkColorSpace set to nullptr. + + Returned SkImageInfo as part of source does not draw, and as part of destination + can not be drawn to. + + @return created SkImageInfo + */ + static SkImageInfo MakeUnknown() { + return MakeUnknown(0, 0); + } + + /** Returns pixel count in each row. + + @return pixel width + */ + int width() const { return fDimensions.width(); } + + /** Returns pixel row count. + + @return pixel height + */ + int height() const { return fDimensions.height(); } + + SkColorType colorType() const { return fColorInfo.colorType(); } + + SkAlphaType alphaType() const { return fColorInfo.alphaType(); } + + /** Returns SkColorSpace, the range of colors. The reference count of + SkColorSpace is unchanged. The returned SkColorSpace is immutable. + + @return SkColorSpace, or nullptr + */ + SkColorSpace* colorSpace() const; + + /** Returns smart pointer to SkColorSpace, the range of colors. The smart pointer + tracks the number of objects sharing this SkColorSpace reference so the memory + is released when the owners destruct. + + The returned SkColorSpace is immutable. + + @return SkColorSpace wrapped in a smart pointer + */ + sk_sp refColorSpace() const; + + /** Returns if SkImageInfo describes an empty area of pixels by checking if either + width or height is zero or smaller. + + @return true if either dimension is zero or smaller + */ + bool isEmpty() const { return fDimensions.isEmpty(); } + + /** Returns the dimensionless SkColorInfo that represents the same color type, + alpha type, and color space as this SkImageInfo. + */ + const SkColorInfo& colorInfo() const { return fColorInfo; } + + /** Returns true if SkAlphaType is set to hint that all pixels are opaque; their + alpha value is implicitly or explicitly 1.0. If true, and all pixels are + not opaque, Skia may draw incorrectly. + + Does not check if SkColorType allows alpha, or if any pixel value has + transparency. + + @return true if SkAlphaType is kOpaque_SkAlphaType + */ + bool isOpaque() const { return fColorInfo.isOpaque(); } + + /** Returns SkISize { width(), height() }. + + @return integral size of width() and height() + */ + SkISize dimensions() const { return fDimensions; } + + /** Returns SkIRect { 0, 0, width(), height() }. + + @return integral rectangle from origin to width() and height() + */ + SkIRect bounds() const { return SkIRect::MakeSize(fDimensions); } + + /** Returns true if associated SkColorSpace is not nullptr, and SkColorSpace gamma + is approximately the same as sRGB. + This includes the + + @return true if SkColorSpace gamma is approximately the same as sRGB + */ + bool gammaCloseToSRGB() const { return fColorInfo.gammaCloseToSRGB(); } + + /** Creates SkImageInfo with the same SkColorType, SkColorSpace, and SkAlphaType, + with dimensions set to width and height. + + @param newWidth pixel column count; must be zero or greater + @param newHeight pixel row count; must be zero or greater + @return created SkImageInfo + */ + SkImageInfo makeWH(int newWidth, int newHeight) const { + return Make({newWidth, newHeight}, fColorInfo); + } + + /** Creates SkImageInfo with the same SkColorType, SkColorSpace, and SkAlphaType, + with dimensions set to newDimensions. + + @param newSize pixel column and row count; must be zero or greater + @return created SkImageInfo + */ + SkImageInfo makeDimensions(SkISize newSize) const { + return Make(newSize, fColorInfo); + } + + /** Creates SkImageInfo with same SkColorType, SkColorSpace, width, and height, + with SkAlphaType set to newAlphaType. + + Created SkImageInfo contains newAlphaType even if it is incompatible with + SkColorType, in which case SkAlphaType in SkImageInfo is ignored. + + @return created SkImageInfo + */ + SkImageInfo makeAlphaType(SkAlphaType newAlphaType) const { + return Make(fDimensions, fColorInfo.makeAlphaType(newAlphaType)); + } + + /** Creates SkImageInfo with same SkAlphaType, SkColorSpace, width, and height, + with SkColorType set to newColorType. + + @return created SkImageInfo + */ + SkImageInfo makeColorType(SkColorType newColorType) const { + return Make(fDimensions, fColorInfo.makeColorType(newColorType)); + } + + /** Creates SkImageInfo with same SkAlphaType, SkColorType, width, and height, + with SkColorSpace set to cs. + + @param cs range of colors; may be nullptr + @return created SkImageInfo + */ + SkImageInfo makeColorSpace(sk_sp cs) const; + + /** Returns number of bytes per pixel required by SkColorType. + Returns zero if colorType( is kUnknown_SkColorType. + + @return bytes in pixel + */ + int bytesPerPixel() const { return fColorInfo.bytesPerPixel(); } + + /** Returns bit shift converting row bytes to row pixels. + Returns zero for kUnknown_SkColorType. + + @return one of: 0, 1, 2, 3; left shift to convert pixels to bytes + */ + int shiftPerPixel() const { return fColorInfo.shiftPerPixel(); } + + /** Returns minimum bytes per row, computed from pixel width() and SkColorType, which + specifies bytesPerPixel(). SkBitmap maximum value for row bytes must fit + in 31 bits. + + @return width() times bytesPerPixel() as unsigned 64-bit integer + */ + uint64_t minRowBytes64() const { + return (uint64_t)sk_64_mul(this->width(), this->bytesPerPixel()); + } + + /** Returns minimum bytes per row, computed from pixel width() and SkColorType, which + specifies bytesPerPixel(). SkBitmap maximum value for row bytes must fit + in 31 bits. + + @return width() times bytesPerPixel() as size_t + */ + size_t minRowBytes() const { + uint64_t minRowBytes = this->minRowBytes64(); + if (!SkTFitsIn(minRowBytes)) { + return 0; + } + return (size_t)minRowBytes; + } + + /** Returns byte offset of pixel from pixel base address. + + Asserts in debug build if x or y is outside of bounds. Does not assert if + rowBytes is smaller than minRowBytes(), even though result may be incorrect. + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @param rowBytes size of pixel row or larger + @return offset within pixel array + + example: https://fiddle.skia.org/c/@ImageInfo_computeOffset + */ + size_t computeOffset(int x, int y, size_t rowBytes) const; + + /** Compares SkImageInfo with other, and returns true if width, height, SkColorType, + SkAlphaType, and SkColorSpace are equivalent. + + @param other SkImageInfo to compare + @return true if SkImageInfo equals other + */ + bool operator==(const SkImageInfo& other) const { + return fDimensions == other.fDimensions && fColorInfo == other.fColorInfo; + } + + /** Compares SkImageInfo with other, and returns true if width, height, SkColorType, + SkAlphaType, and SkColorSpace are not equivalent. + + @param other SkImageInfo to compare + @return true if SkImageInfo is not equal to other + */ + bool operator!=(const SkImageInfo& other) const { + return !(*this == other); + } + + /** Returns storage required by pixel array, given SkImageInfo dimensions, SkColorType, + and rowBytes. rowBytes is assumed to be at least as large as minRowBytes(). + + Returns zero if height is zero. + Returns SIZE_MAX if answer exceeds the range of size_t. + + @param rowBytes size of pixel row or larger + @return memory required by pixel buffer + */ + size_t computeByteSize(size_t rowBytes) const; + + /** Returns storage required by pixel array, given SkImageInfo dimensions, and + SkColorType. Uses minRowBytes() to compute bytes for pixel row. + + Returns zero if height is zero. + Returns SIZE_MAX if answer exceeds the range of size_t. + + @return least memory required by pixel buffer + */ + size_t computeMinByteSize() const { + return this->computeByteSize(this->minRowBytes()); + } + + /** Returns true if byteSize equals SIZE_MAX. computeByteSize() and + computeMinByteSize() return SIZE_MAX if size_t can not hold buffer size. + + @param byteSize result of computeByteSize() or computeMinByteSize() + @return true if computeByteSize() or computeMinByteSize() result exceeds size_t + */ + static bool ByteSizeOverflowed(size_t byteSize) { + return SIZE_MAX == byteSize; + } + + /** Returns true if rowBytes is valid for this SkImageInfo. + + @param rowBytes size of pixel row including padding + @return true if rowBytes is large enough to contain pixel row and is properly + aligned + */ + bool validRowBytes(size_t rowBytes) const { + if (rowBytes < this->minRowBytes64()) { + return false; + } + int shift = this->shiftPerPixel(); + size_t alignedRowBytes = rowBytes >> shift << shift; + return alignedRowBytes == rowBytes; + } + + /** Creates an empty SkImageInfo with kUnknown_SkColorType, kUnknown_SkAlphaType, + a width and height of zero, and no SkColorSpace. + */ + void reset() { *this = {}; } + + /** Asserts if internal values are illegal or inconsistent. Only available if + SK_DEBUG is defined at compile time. + */ + SkDEBUGCODE(void validate() const;) + +private: + SkColorInfo fColorInfo; + SkISize fDimensions = {0, 0}; + + SkImageInfo(SkISize dimensions, const SkColorInfo& colorInfo) + : fColorInfo(colorInfo), fDimensions(dimensions) {} + + SkImageInfo(SkISize dimensions, SkColorInfo&& colorInfo) + : fColorInfo(std::move(colorInfo)), fDimensions(dimensions) {} +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkM44.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkM44.h new file mode 100644 index 00000000000..d8c77ea07a5 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkM44.h @@ -0,0 +1,442 @@ +/* + * Copyright 2020 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkM44_DEFINED +#define SkM44_DEFINED + +#include "include/core/SkMatrix.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" + +#include + +struct SkRect; + +struct SK_API SkV2 { + float x, y; + + bool operator==(const SkV2 v) const { return x == v.x && y == v.y; } + bool operator!=(const SkV2 v) const { return !(*this == v); } + + static SkScalar Dot(SkV2 a, SkV2 b) { return a.x * b.x + a.y * b.y; } + static SkScalar Cross(SkV2 a, SkV2 b) { return a.x * b.y - a.y * b.x; } + static SkV2 Normalize(SkV2 v) { return v * (1.0f / v.length()); } + + SkV2 operator-() const { return {-x, -y}; } + SkV2 operator+(SkV2 v) const { return {x+v.x, y+v.y}; } + SkV2 operator-(SkV2 v) const { return {x-v.x, y-v.y}; } + + SkV2 operator*(SkV2 v) const { return {x*v.x, y*v.y}; } + friend SkV2 operator*(SkV2 v, SkScalar s) { return {v.x*s, v.y*s}; } + friend SkV2 operator*(SkScalar s, SkV2 v) { return {v.x*s, v.y*s}; } + friend SkV2 operator/(SkV2 v, SkScalar s) { return {v.x/s, v.y/s}; } + friend SkV2 operator/(SkScalar s, SkV2 v) { return {s/v.x, s/v.y}; } + + void operator+=(SkV2 v) { *this = *this + v; } + void operator-=(SkV2 v) { *this = *this - v; } + void operator*=(SkV2 v) { *this = *this * v; } + void operator*=(SkScalar s) { *this = *this * s; } + void operator/=(SkScalar s) { *this = *this / s; } + + SkScalar lengthSquared() const { return Dot(*this, *this); } + SkScalar length() const { return SkScalarSqrt(this->lengthSquared()); } + + SkScalar dot(SkV2 v) const { return Dot(*this, v); } + SkScalar cross(SkV2 v) const { return Cross(*this, v); } + SkV2 normalize() const { return Normalize(*this); } + + const float* ptr() const { return &x; } + float* ptr() { return &x; } +}; + +struct SK_API SkV3 { + float x, y, z; + + bool operator==(const SkV3& v) const { + return x == v.x && y == v.y && z == v.z; + } + bool operator!=(const SkV3& v) const { return !(*this == v); } + + static SkScalar Dot(const SkV3& a, const SkV3& b) { return a.x*b.x + a.y*b.y + a.z*b.z; } + static SkV3 Cross(const SkV3& a, const SkV3& b) { + return { a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x }; + } + static SkV3 Normalize(const SkV3& v) { return v * (1.0f / v.length()); } + + SkV3 operator-() const { return {-x, -y, -z}; } + SkV3 operator+(const SkV3& v) const { return { x + v.x, y + v.y, z + v.z }; } + SkV3 operator-(const SkV3& v) const { return { x - v.x, y - v.y, z - v.z }; } + + SkV3 operator*(const SkV3& v) const { + return { x*v.x, y*v.y, z*v.z }; + } + friend SkV3 operator*(const SkV3& v, SkScalar s) { + return { v.x*s, v.y*s, v.z*s }; + } + friend SkV3 operator*(SkScalar s, const SkV3& v) { return v*s; } + + void operator+=(SkV3 v) { *this = *this + v; } + void operator-=(SkV3 v) { *this = *this - v; } + void operator*=(SkV3 v) { *this = *this * v; } + void operator*=(SkScalar s) { *this = *this * s; } + + SkScalar lengthSquared() const { return Dot(*this, *this); } + SkScalar length() const { return SkScalarSqrt(Dot(*this, *this)); } + + SkScalar dot(const SkV3& v) const { return Dot(*this, v); } + SkV3 cross(const SkV3& v) const { return Cross(*this, v); } + SkV3 normalize() const { return Normalize(*this); } + + const float* ptr() const { return &x; } + float* ptr() { return &x; } +}; + +struct SK_API SkV4 { + float x, y, z, w; + + bool operator==(const SkV4& v) const { + return x == v.x && y == v.y && z == v.z && w == v.w; + } + bool operator!=(const SkV4& v) const { return !(*this == v); } + + static SkScalar Dot(const SkV4& a, const SkV4& b) { + return a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w; + } + static SkV4 Normalize(const SkV4& v) { return v * (1.0f / v.length()); } + + SkV4 operator-() const { return {-x, -y, -z, -w}; } + SkV4 operator+(const SkV4& v) const { return { x + v.x, y + v.y, z + v.z, w + v.w }; } + SkV4 operator-(const SkV4& v) const { return { x - v.x, y - v.y, z - v.z, w - v.w }; } + + SkV4 operator*(const SkV4& v) const { + return { x*v.x, y*v.y, z*v.z, w*v.w }; + } + friend SkV4 operator*(const SkV4& v, SkScalar s) { + return { v.x*s, v.y*s, v.z*s, v.w*s }; + } + friend SkV4 operator*(SkScalar s, const SkV4& v) { return v*s; } + + SkScalar lengthSquared() const { return Dot(*this, *this); } + SkScalar length() const { return SkScalarSqrt(Dot(*this, *this)); } + + SkScalar dot(const SkV4& v) const { return Dot(*this, v); } + SkV4 normalize() const { return Normalize(*this); } + + const float* ptr() const { return &x; } + float* ptr() { return &x; } + + float operator[](int i) const { + SkASSERT(i >= 0 && i < 4); + return this->ptr()[i]; + } + float& operator[](int i) { + SkASSERT(i >= 0 && i < 4); + return this->ptr()[i]; + } +}; + +/** + * 4x4 matrix used by SkCanvas and other parts of Skia. + * + * Skia assumes a right-handed coordinate system: + * +X goes to the right + * +Y goes down + * +Z goes into the screen (away from the viewer) + */ +class SK_API SkM44 { +public: + SkM44(const SkM44& src) = default; + SkM44& operator=(const SkM44& src) = default; + + constexpr SkM44() + : fMat{1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1} + {} + + SkM44(const SkM44& a, const SkM44& b) { + this->setConcat(a, b); + } + + enum Uninitialized_Constructor { + kUninitialized_Constructor + }; + SkM44(Uninitialized_Constructor) {} + + enum NaN_Constructor { + kNaN_Constructor + }; + constexpr SkM44(NaN_Constructor) + : fMat{SK_ScalarNaN, SK_ScalarNaN, SK_ScalarNaN, SK_ScalarNaN, + SK_ScalarNaN, SK_ScalarNaN, SK_ScalarNaN, SK_ScalarNaN, + SK_ScalarNaN, SK_ScalarNaN, SK_ScalarNaN, SK_ScalarNaN, + SK_ScalarNaN, SK_ScalarNaN, SK_ScalarNaN, SK_ScalarNaN} + {} + + /** + * The constructor parameters are in row-major order. + */ + constexpr SkM44(SkScalar m0, SkScalar m4, SkScalar m8, SkScalar m12, + SkScalar m1, SkScalar m5, SkScalar m9, SkScalar m13, + SkScalar m2, SkScalar m6, SkScalar m10, SkScalar m14, + SkScalar m3, SkScalar m7, SkScalar m11, SkScalar m15) + // fMat is column-major order in memory. + : fMat{m0, m1, m2, m3, + m4, m5, m6, m7, + m8, m9, m10, m11, + m12, m13, m14, m15} + {} + + static SkM44 Rows(const SkV4& r0, const SkV4& r1, const SkV4& r2, const SkV4& r3) { + SkM44 m(kUninitialized_Constructor); + m.setRow(0, r0); + m.setRow(1, r1); + m.setRow(2, r2); + m.setRow(3, r3); + return m; + } + static SkM44 Cols(const SkV4& c0, const SkV4& c1, const SkV4& c2, const SkV4& c3) { + SkM44 m(kUninitialized_Constructor); + m.setCol(0, c0); + m.setCol(1, c1); + m.setCol(2, c2); + m.setCol(3, c3); + return m; + } + + static SkM44 RowMajor(const SkScalar r[16]) { + return SkM44(r[ 0], r[ 1], r[ 2], r[ 3], + r[ 4], r[ 5], r[ 6], r[ 7], + r[ 8], r[ 9], r[10], r[11], + r[12], r[13], r[14], r[15]); + } + static SkM44 ColMajor(const SkScalar c[16]) { + return SkM44(c[0], c[4], c[ 8], c[12], + c[1], c[5], c[ 9], c[13], + c[2], c[6], c[10], c[14], + c[3], c[7], c[11], c[15]); + } + + static SkM44 Translate(SkScalar x, SkScalar y, SkScalar z = 0) { + return SkM44(1, 0, 0, x, + 0, 1, 0, y, + 0, 0, 1, z, + 0, 0, 0, 1); + } + + static SkM44 Scale(SkScalar x, SkScalar y, SkScalar z = 1) { + return SkM44(x, 0, 0, 0, + 0, y, 0, 0, + 0, 0, z, 0, + 0, 0, 0, 1); + } + + static SkM44 Rotate(SkV3 axis, SkScalar radians) { + SkM44 m(kUninitialized_Constructor); + m.setRotate(axis, radians); + return m; + } + + // Scales and translates 'src' to fill 'dst' exactly. + static SkM44 RectToRect(const SkRect& src, const SkRect& dst); + + static SkM44 LookAt(const SkV3& eye, const SkV3& center, const SkV3& up); + static SkM44 Perspective(float near, float far, float angle); + + bool operator==(const SkM44& other) const; + bool operator!=(const SkM44& other) const { + return !(other == *this); + } + + void getColMajor(SkScalar v[]) const { + memcpy(v, fMat, sizeof(fMat)); + } + void getRowMajor(SkScalar v[]) const; + + SkScalar rc(int r, int c) const { + SkASSERT(r >= 0 && r <= 3); + SkASSERT(c >= 0 && c <= 3); + return fMat[c*4 + r]; + } + void setRC(int r, int c, SkScalar value) { + SkASSERT(r >= 0 && r <= 3); + SkASSERT(c >= 0 && c <= 3); + fMat[c*4 + r] = value; + } + + SkV4 row(int i) const { + SkASSERT(i >= 0 && i <= 3); + return {fMat[i + 0], fMat[i + 4], fMat[i + 8], fMat[i + 12]}; + } + SkV4 col(int i) const { + SkASSERT(i >= 0 && i <= 3); + return {fMat[i*4 + 0], fMat[i*4 + 1], fMat[i*4 + 2], fMat[i*4 + 3]}; + } + + void setRow(int i, const SkV4& v) { + SkASSERT(i >= 0 && i <= 3); + fMat[i + 0] = v.x; + fMat[i + 4] = v.y; + fMat[i + 8] = v.z; + fMat[i + 12] = v.w; + } + void setCol(int i, const SkV4& v) { + SkASSERT(i >= 0 && i <= 3); + memcpy(&fMat[i*4], v.ptr(), sizeof(v)); + } + + SkM44& setIdentity() { + *this = { 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 }; + return *this; + } + + SkM44& setTranslate(SkScalar x, SkScalar y, SkScalar z = 0) { + *this = { 1, 0, 0, x, + 0, 1, 0, y, + 0, 0, 1, z, + 0, 0, 0, 1 }; + return *this; + } + + SkM44& setScale(SkScalar x, SkScalar y, SkScalar z = 1) { + *this = { x, 0, 0, 0, + 0, y, 0, 0, + 0, 0, z, 0, + 0, 0, 0, 1 }; + return *this; + } + + /** + * Set this matrix to rotate about the specified unit-length axis vector, + * by an angle specified by its sin() and cos(). + * + * This does not attempt to verify that axis.length() == 1 or that the sin,cos values + * are correct. + */ + SkM44& setRotateUnitSinCos(SkV3 axis, SkScalar sinAngle, SkScalar cosAngle); + + /** + * Set this matrix to rotate about the specified unit-length axis vector, + * by an angle specified in radians. + * + * This does not attempt to verify that axis.length() == 1. + */ + SkM44& setRotateUnit(SkV3 axis, SkScalar radians) { + return this->setRotateUnitSinCos(axis, SkScalarSin(radians), SkScalarCos(radians)); + } + + /** + * Set this matrix to rotate about the specified axis vector, + * by an angle specified in radians. + * + * Note: axis is not assumed to be unit-length, so it will be normalized internally. + * If axis is already unit-length, call setRotateAboutUnitRadians() instead. + */ + SkM44& setRotate(SkV3 axis, SkScalar radians); + + SkM44& setConcat(const SkM44& a, const SkM44& b); + + friend SkM44 operator*(const SkM44& a, const SkM44& b) { + return SkM44(a, b); + } + + SkM44& preConcat(const SkM44& m) { + return this->setConcat(*this, m); + } + + SkM44& postConcat(const SkM44& m) { + return this->setConcat(m, *this); + } + + /** + * A matrix is categorized as 'perspective' if the bottom row is not [0, 0, 0, 1]. + * For most uses, a bottom row of [0, 0, 0, X] behaves like a non-perspective matrix, though + * it will be categorized as perspective. Calling normalizePerspective() will change the + * matrix such that, if its bottom row was [0, 0, 0, X], it will be changed to [0, 0, 0, 1] + * by scaling the rest of the matrix by 1/X. + * + * | A B C D | | A/X B/X C/X D/X | + * | E F G H | -> | E/X F/X G/X H/X | for X != 0 + * | I J K L | | I/X J/X K/X L/X | + * | 0 0 0 X | | 0 0 0 1 | + */ + void normalizePerspective(); + + /** Returns true if all elements of the matrix are finite. Returns false if any + element is infinity, or NaN. + + @return true if matrix has only finite elements + */ + bool isFinite() const { return SkIsFinite(fMat, 16); } + + /** If this is invertible, return that in inverse and return true. If it is + * not invertible, return false and leave the inverse parameter unchanged. + */ + [[nodiscard]] bool invert(SkM44* inverse) const; + + [[nodiscard]] SkM44 transpose() const; + + void dump() const; + + //////////// + + SkV4 map(float x, float y, float z, float w) const; + SkV4 operator*(const SkV4& v) const { + return this->map(v.x, v.y, v.z, v.w); + } + SkV3 operator*(SkV3 v) const { + auto v4 = this->map(v.x, v.y, v.z, 0); + return {v4.x, v4.y, v4.z}; + } + ////////////////////// Converting to/from SkMatrix + + /* When converting from SkM44 to SkMatrix, the third row and + * column is dropped. When converting from SkMatrix to SkM44 + * the third row and column remain as identity: + * [ a b c ] [ a b 0 c ] + * [ d e f ] -> [ d e 0 f ] + * [ g h i ] [ 0 0 1 0 ] + * [ g h 0 i ] + */ + SkMatrix asM33() const { + return SkMatrix::MakeAll(fMat[0], fMat[4], fMat[12], + fMat[1], fMat[5], fMat[13], + fMat[3], fMat[7], fMat[15]); + } + + explicit SkM44(const SkMatrix& src) + : SkM44(src[SkMatrix::kMScaleX], src[SkMatrix::kMSkewX], 0, src[SkMatrix::kMTransX], + src[SkMatrix::kMSkewY], src[SkMatrix::kMScaleY], 0, src[SkMatrix::kMTransY], + 0, 0, 1, 0, + src[SkMatrix::kMPersp0], src[SkMatrix::kMPersp1], 0, src[SkMatrix::kMPersp2]) + {} + + SkM44& preTranslate(SkScalar x, SkScalar y, SkScalar z = 0); + SkM44& postTranslate(SkScalar x, SkScalar y, SkScalar z = 0); + + SkM44& preScale(SkScalar x, SkScalar y); + SkM44& preScale(SkScalar x, SkScalar y, SkScalar z); + SkM44& preConcat(const SkMatrix&); + +private: + /* Stored in column-major. + * Indices + * 0 4 8 12 1 0 0 trans_x + * 1 5 9 13 e.g. 0 1 0 trans_y + * 2 6 10 14 0 0 1 trans_z + * 3 7 11 15 0 0 0 1 + */ + SkScalar fMat[16]; + + friend class SkMatrixPriv; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMallocPixelRef.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMallocPixelRef.h new file mode 100644 index 00000000000..5f373485830 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMallocPixelRef.h @@ -0,0 +1,45 @@ +/* + * Copyright 2008 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkMallocPixelRef_DEFINED +#define SkMallocPixelRef_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" + +#include + +class SkData; +class SkPixelRef; +struct SkImageInfo; + +/** We explicitly use the same allocator for our pixels that SkMask does, + so that we can freely assign memory allocated by one class to the other. +*/ +namespace SkMallocPixelRef { + /** + * Return a new SkMallocPixelRef, automatically allocating storage for the + * pixels. If rowBytes are 0, an optimal value will be chosen automatically. + * If rowBytes is > 0, then it will be respected, or NULL will be returned + * if rowBytes is invalid for the specified info. + * + * All pixel bytes are zeroed. + * + * Returns NULL on failure. + */ + SK_API sk_sp MakeAllocate(const SkImageInfo&, size_t rowBytes); + + /** + * Return a new SkMallocPixelRef that will use the provided SkData and + * rowBytes as pixel storage. The SkData will be ref()ed and on + * destruction of the PixelRef, the SkData will be unref()ed. + * + * Returns NULL on failure. + */ + SK_API sk_sp MakeWithData(const SkImageInfo&, size_t rowBytes, sk_sp data); +} // namespace SkMallocPixelRef +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMaskFilter.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMaskFilter.h new file mode 100644 index 00000000000..9d03e98c0c5 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMaskFilter.h @@ -0,0 +1,53 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkMaskFilter_DEFINED +#define SkMaskFilter_DEFINED + +#include "include/core/SkFlattenable.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" + +#include + +enum SkBlurStyle : int; +struct SkDeserialProcs; +struct SkRect; + +/** \class SkMaskFilter + + SkMaskFilter is the base class for object that perform transformations on + the mask before drawing it. An example subclass is Blur. +*/ +class SK_API SkMaskFilter : public SkFlattenable { +public: + /** Create a blur maskfilter. + * @param style The SkBlurStyle to use + * @param sigma Standard deviation of the Gaussian blur to apply. Must be > 0. + * @param respectCTM if true the blur's sigma is modified by the CTM. + * @return The new blur maskfilter + */ + static sk_sp MakeBlur(SkBlurStyle style, SkScalar sigma, + bool respectCTM = true); + + /** + * Returns the approximate bounds that would result from filtering the src rect. + * The actual result may be different, but it should be contained within the + * returned bounds. + */ + SkRect approximateFilteredBounds(const SkRect& src) const; + + static sk_sp Deserialize(const void* data, size_t size, + const SkDeserialProcs* procs = nullptr); + +private: + static void RegisterFlattenables(); + friend class SkFlattenable; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMatrix.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMatrix.h new file mode 100644 index 00000000000..10ee699db16 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMatrix.h @@ -0,0 +1,1997 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkMatrix_DEFINED +#define SkMatrix_DEFINED + +#include "include/core/SkPoint.h" +#include "include/core/SkRect.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkFloatingPoint.h" +#include "include/private/base/SkMacros.h" +#include "include/private/base/SkTo.h" + +#include +#include + +struct SkPoint3; +struct SkRSXform; +struct SkSize; + +// Remove when clients are updated to live without this +#define SK_SUPPORT_LEGACY_MATRIX_RECTTORECT + +/** + * When we transform points through a matrix containing perspective (the bottom row is something + * other than 0,0,1), the bruteforce math can produce confusing results (since we might divide + * by 0, or a negative w value). By default, methods that map rects and paths will apply + * perspective clipping, but this can be changed by specifying kYes to those methods. + */ +enum class SkApplyPerspectiveClip { + kNo, //!< Don't pre-clip the geometry before applying the (perspective) matrix + kYes, //!< Do pre-clip the geometry before applying the (perspective) matrix +}; + +/** \class SkMatrix + SkMatrix holds a 3x3 matrix for transforming coordinates. This allows mapping + SkPoint and vectors with translation, scaling, skewing, rotation, and + perspective. + + SkMatrix elements are in row major order. + SkMatrix constexpr default constructs to identity. + + SkMatrix includes a hidden variable that classifies the type of matrix to + improve performance. SkMatrix is not thread safe unless getType() is called first. + + example: https://fiddle.skia.org/c/@Matrix_063 +*/ +SK_BEGIN_REQUIRE_DENSE +class SK_API SkMatrix { +public: + + /** Creates an identity SkMatrix: + + | 1 0 0 | + | 0 1 0 | + | 0 0 1 | + */ + constexpr SkMatrix() : SkMatrix(1,0,0, 0,1,0, 0,0,1, kIdentity_Mask | kRectStaysRect_Mask) {} + + /** Sets SkMatrix to scale by (sx, sy). Returned matrix is: + + | sx 0 0 | + | 0 sy 0 | + | 0 0 1 | + + @param sx horizontal scale factor + @param sy vertical scale factor + @return SkMatrix with scale + */ + [[nodiscard]] static SkMatrix Scale(SkScalar sx, SkScalar sy) { + SkMatrix m; + m.setScale(sx, sy); + return m; + } + + /** Sets SkMatrix to translate by (dx, dy). Returned matrix is: + + | 1 0 dx | + | 0 1 dy | + | 0 0 1 | + + @param dx horizontal translation + @param dy vertical translation + @return SkMatrix with translation + */ + [[nodiscard]] static SkMatrix Translate(SkScalar dx, SkScalar dy) { + SkMatrix m; + m.setTranslate(dx, dy); + return m; + } + [[nodiscard]] static SkMatrix Translate(SkVector t) { return Translate(t.x(), t.y()); } + [[nodiscard]] static SkMatrix Translate(SkIVector t) { return Translate(t.x(), t.y()); } + + /** Sets SkMatrix to rotate by |deg| about a pivot point at (0, 0). + + @param deg rotation angle in degrees (positive rotates clockwise) + @return SkMatrix with rotation + */ + [[nodiscard]] static SkMatrix RotateDeg(SkScalar deg) { + SkMatrix m; + m.setRotate(deg); + return m; + } + [[nodiscard]] static SkMatrix RotateDeg(SkScalar deg, SkPoint pt) { + SkMatrix m; + m.setRotate(deg, pt.x(), pt.y()); + return m; + } + [[nodiscard]] static SkMatrix RotateRad(SkScalar rad) { + return RotateDeg(SkRadiansToDegrees(rad)); + } + + /** Sets SkMatrix to skew by (kx, ky) about pivot point (0, 0). + + @param kx horizontal skew factor + @param ky vertical skew factor + @return SkMatrix with skew + */ + [[nodiscard]] static SkMatrix Skew(SkScalar kx, SkScalar ky) { + SkMatrix m; + m.setSkew(kx, ky); + return m; + } + + /** \enum SkMatrix::ScaleToFit + ScaleToFit describes how SkMatrix is constructed to map one SkRect to another. + ScaleToFit may allow SkMatrix to have unequal horizontal and vertical scaling, + or may restrict SkMatrix to square scaling. If restricted, ScaleToFit specifies + how SkMatrix maps to the side or center of the destination SkRect. + */ + enum ScaleToFit { + kFill_ScaleToFit, //!< scales in x and y to fill destination SkRect + kStart_ScaleToFit, //!< scales and aligns to left and top + kCenter_ScaleToFit, //!< scales and aligns to center + kEnd_ScaleToFit, //!< scales and aligns to right and bottom + }; + + /** Returns SkMatrix set to scale and translate src to dst. ScaleToFit selects + whether mapping completely fills dst or preserves the aspect ratio, and how to + align src within dst. Returns the identity SkMatrix if src is empty. If dst is + empty, returns SkMatrix set to: + + | 0 0 0 | + | 0 0 0 | + | 0 0 1 | + + @param src SkRect to map from + @param dst SkRect to map to + @param mode How to handle the mapping + @return SkMatrix mapping src to dst + */ + [[nodiscard]] static SkMatrix RectToRect(const SkRect& src, const SkRect& dst, + ScaleToFit mode = kFill_ScaleToFit) { + return MakeRectToRect(src, dst, mode); + } + + /** Sets SkMatrix to: + + | scaleX skewX transX | + | skewY scaleY transY | + | pers0 pers1 pers2 | + + @param scaleX horizontal scale factor + @param skewX horizontal skew factor + @param transX horizontal translation + @param skewY vertical skew factor + @param scaleY vertical scale factor + @param transY vertical translation + @param pers0 input x-axis perspective factor + @param pers1 input y-axis perspective factor + @param pers2 perspective scale factor + @return SkMatrix constructed from parameters + */ + [[nodiscard]] static SkMatrix MakeAll(SkScalar scaleX, SkScalar skewX, SkScalar transX, + SkScalar skewY, SkScalar scaleY, SkScalar transY, + SkScalar pers0, SkScalar pers1, SkScalar pers2) { + SkMatrix m; + m.setAll(scaleX, skewX, transX, skewY, scaleY, transY, pers0, pers1, pers2); + return m; + } + + /** \enum SkMatrix::TypeMask + Enum of bit fields for mask returned by getType(). + Used to identify the complexity of SkMatrix, to optimize performance. + */ + enum TypeMask { + kIdentity_Mask = 0, //!< identity SkMatrix; all bits clear + kTranslate_Mask = 0x01, //!< translation SkMatrix + kScale_Mask = 0x02, //!< scale SkMatrix + kAffine_Mask = 0x04, //!< skew or rotate SkMatrix + kPerspective_Mask = 0x08, //!< perspective SkMatrix + }; + + /** Returns a bit field describing the transformations the matrix may + perform. The bit field is computed conservatively, so it may include + false positives. For example, when kPerspective_Mask is set, all + other bits are set. + + @return kIdentity_Mask, or combinations of: kTranslate_Mask, kScale_Mask, + kAffine_Mask, kPerspective_Mask + */ + TypeMask getType() const { + if (fTypeMask & kUnknown_Mask) { + fTypeMask = this->computeTypeMask(); + } + // only return the public masks + return (TypeMask)(fTypeMask & 0xF); + } + + /** Returns true if SkMatrix is identity. Identity matrix is: + + | 1 0 0 | + | 0 1 0 | + | 0 0 1 | + + @return true if SkMatrix has no effect + */ + bool isIdentity() const { + return this->getType() == 0; + } + + /** Returns true if SkMatrix at most scales and translates. SkMatrix may be identity, + contain only scale elements, only translate elements, or both. SkMatrix form is: + + | scale-x 0 translate-x | + | 0 scale-y translate-y | + | 0 0 1 | + + @return true if SkMatrix is identity; or scales, translates, or both + */ + bool isScaleTranslate() const { + return !(this->getType() & ~(kScale_Mask | kTranslate_Mask)); + } + + /** Returns true if SkMatrix is identity, or translates. SkMatrix form is: + + | 1 0 translate-x | + | 0 1 translate-y | + | 0 0 1 | + + @return true if SkMatrix is identity, or translates + */ + bool isTranslate() const { return !(this->getType() & ~(kTranslate_Mask)); } + + /** Returns true SkMatrix maps SkRect to another SkRect. If true, SkMatrix is identity, + or scales, or rotates a multiple of 90 degrees, or mirrors on axes. In all + cases, SkMatrix may also have translation. SkMatrix form is either: + + | scale-x 0 translate-x | + | 0 scale-y translate-y | + | 0 0 1 | + + or + + | 0 rotate-x translate-x | + | rotate-y 0 translate-y | + | 0 0 1 | + + for non-zero values of scale-x, scale-y, rotate-x, and rotate-y. + + Also called preservesAxisAlignment(); use the one that provides better inline + documentation. + + @return true if SkMatrix maps one SkRect into another + */ + bool rectStaysRect() const { + if (fTypeMask & kUnknown_Mask) { + fTypeMask = this->computeTypeMask(); + } + return (fTypeMask & kRectStaysRect_Mask) != 0; + } + + /** Returns true SkMatrix maps SkRect to another SkRect. If true, SkMatrix is identity, + or scales, or rotates a multiple of 90 degrees, or mirrors on axes. In all + cases, SkMatrix may also have translation. SkMatrix form is either: + + | scale-x 0 translate-x | + | 0 scale-y translate-y | + | 0 0 1 | + + or + + | 0 rotate-x translate-x | + | rotate-y 0 translate-y | + | 0 0 1 | + + for non-zero values of scale-x, scale-y, rotate-x, and rotate-y. + + Also called rectStaysRect(); use the one that provides better inline + documentation. + + @return true if SkMatrix maps one SkRect into another + */ + bool preservesAxisAlignment() const { return this->rectStaysRect(); } + + /** Returns true if the matrix contains perspective elements. SkMatrix form is: + + | -- -- -- | + | -- -- -- | + | perspective-x perspective-y perspective-scale | + + where perspective-x or perspective-y is non-zero, or perspective-scale is + not one. All other elements may have any value. + + @return true if SkMatrix is in most general form + */ + bool hasPerspective() const { + return SkToBool(this->getPerspectiveTypeMaskOnly() & + kPerspective_Mask); + } + + /** Returns true if SkMatrix contains only translation, rotation, reflection, and + uniform scale. + Returns false if SkMatrix contains different scales, skewing, perspective, or + degenerate forms that collapse to a line or point. + + Describes that the SkMatrix makes rendering with and without the matrix are + visually alike; a transformed circle remains a circle. Mathematically, this is + referred to as similarity of a Euclidean space, or a similarity transformation. + + Preserves right angles, keeping the arms of the angle equal lengths. + + @param tol to be deprecated + @return true if SkMatrix only rotates, uniformly scales, translates + + example: https://fiddle.skia.org/c/@Matrix_isSimilarity + */ + bool isSimilarity(SkScalar tol = SK_ScalarNearlyZero) const; + + /** Returns true if SkMatrix contains only translation, rotation, reflection, and + scale. Scale may differ along rotated axes. + Returns false if SkMatrix skewing, perspective, or degenerate forms that collapse + to a line or point. + + Preserves right angles, but not requiring that the arms of the angle + retain equal lengths. + + @param tol to be deprecated + @return true if SkMatrix only rotates, scales, translates + + example: https://fiddle.skia.org/c/@Matrix_preservesRightAngles + */ + bool preservesRightAngles(SkScalar tol = SK_ScalarNearlyZero) const; + + /** SkMatrix organizes its values in row-major order. These members correspond to + each value in SkMatrix. + */ + static constexpr int kMScaleX = 0; //!< horizontal scale factor + static constexpr int kMSkewX = 1; //!< horizontal skew factor + static constexpr int kMTransX = 2; //!< horizontal translation + static constexpr int kMSkewY = 3; //!< vertical skew factor + static constexpr int kMScaleY = 4; //!< vertical scale factor + static constexpr int kMTransY = 5; //!< vertical translation + static constexpr int kMPersp0 = 6; //!< input x perspective factor + static constexpr int kMPersp1 = 7; //!< input y perspective factor + static constexpr int kMPersp2 = 8; //!< perspective bias + + /** Affine arrays are in column-major order to match the matrix used by + PDF and XPS. + */ + static constexpr int kAScaleX = 0; //!< horizontal scale factor + static constexpr int kASkewY = 1; //!< vertical skew factor + static constexpr int kASkewX = 2; //!< horizontal skew factor + static constexpr int kAScaleY = 3; //!< vertical scale factor + static constexpr int kATransX = 4; //!< horizontal translation + static constexpr int kATransY = 5; //!< vertical translation + + /** Returns one matrix value. Asserts if index is out of range and SK_DEBUG is + defined. + + @param index one of: kMScaleX, kMSkewX, kMTransX, kMSkewY, kMScaleY, kMTransY, + kMPersp0, kMPersp1, kMPersp2 + @return value corresponding to index + */ + SkScalar operator[](int index) const { + SkASSERT((unsigned)index < 9); + return fMat[index]; + } + + /** Returns one matrix value. Asserts if index is out of range and SK_DEBUG is + defined. + + @param index one of: kMScaleX, kMSkewX, kMTransX, kMSkewY, kMScaleY, kMTransY, + kMPersp0, kMPersp1, kMPersp2 + @return value corresponding to index + */ + SkScalar get(int index) const { + SkASSERT((unsigned)index < 9); + return fMat[index]; + } + + /** Returns one matrix value from a particular row/column. Asserts if index is out + of range and SK_DEBUG is defined. + + @param r matrix row to fetch + @param c matrix column to fetch + @return value at the given matrix position + */ + SkScalar rc(int r, int c) const { + SkASSERT(r >= 0 && r <= 2); + SkASSERT(c >= 0 && c <= 2); + return fMat[r*3 + c]; + } + + /** Returns scale factor multiplied by x-axis input, contributing to x-axis output. + With mapPoints(), scales SkPoint along the x-axis. + + @return horizontal scale factor + */ + SkScalar getScaleX() const { return fMat[kMScaleX]; } + + /** Returns scale factor multiplied by y-axis input, contributing to y-axis output. + With mapPoints(), scales SkPoint along the y-axis. + + @return vertical scale factor + */ + SkScalar getScaleY() const { return fMat[kMScaleY]; } + + /** Returns scale factor multiplied by x-axis input, contributing to y-axis output. + With mapPoints(), skews SkPoint along the y-axis. + Skewing both axes can rotate SkPoint. + + @return vertical skew factor + */ + SkScalar getSkewY() const { return fMat[kMSkewY]; } + + /** Returns scale factor multiplied by y-axis input, contributing to x-axis output. + With mapPoints(), skews SkPoint along the x-axis. + Skewing both axes can rotate SkPoint. + + @return horizontal scale factor + */ + SkScalar getSkewX() const { return fMat[kMSkewX]; } + + /** Returns translation contributing to x-axis output. + With mapPoints(), moves SkPoint along the x-axis. + + @return horizontal translation factor + */ + SkScalar getTranslateX() const { return fMat[kMTransX]; } + + /** Returns translation contributing to y-axis output. + With mapPoints(), moves SkPoint along the y-axis. + + @return vertical translation factor + */ + SkScalar getTranslateY() const { return fMat[kMTransY]; } + + /** Returns factor scaling input x-axis relative to input y-axis. + + @return input x-axis perspective factor + */ + SkScalar getPerspX() const { return fMat[kMPersp0]; } + + /** Returns factor scaling input y-axis relative to input x-axis. + + @return input y-axis perspective factor + */ + SkScalar getPerspY() const { return fMat[kMPersp1]; } + + /** Returns writable SkMatrix value. Asserts if index is out of range and SK_DEBUG is + defined. Clears internal cache anticipating that caller will change SkMatrix value. + + Next call to read SkMatrix state may recompute cache; subsequent writes to SkMatrix + value must be followed by dirtyMatrixTypeCache(). + + @param index one of: kMScaleX, kMSkewX, kMTransX, kMSkewY, kMScaleY, kMTransY, + kMPersp0, kMPersp1, kMPersp2 + @return writable value corresponding to index + */ + SkScalar& operator[](int index) { + SkASSERT((unsigned)index < 9); + this->setTypeMask(kUnknown_Mask); + return fMat[index]; + } + + /** Sets SkMatrix value. Asserts if index is out of range and SK_DEBUG is + defined. Safer than operator[]; internal cache is always maintained. + + @param index one of: kMScaleX, kMSkewX, kMTransX, kMSkewY, kMScaleY, kMTransY, + kMPersp0, kMPersp1, kMPersp2 + @param value scalar to store in SkMatrix + */ + SkMatrix& set(int index, SkScalar value) { + SkASSERT((unsigned)index < 9); + fMat[index] = value; + this->setTypeMask(kUnknown_Mask); + return *this; + } + + /** Sets horizontal scale factor. + + @param v horizontal scale factor to store + */ + SkMatrix& setScaleX(SkScalar v) { return this->set(kMScaleX, v); } + + /** Sets vertical scale factor. + + @param v vertical scale factor to store + */ + SkMatrix& setScaleY(SkScalar v) { return this->set(kMScaleY, v); } + + /** Sets vertical skew factor. + + @param v vertical skew factor to store + */ + SkMatrix& setSkewY(SkScalar v) { return this->set(kMSkewY, v); } + + /** Sets horizontal skew factor. + + @param v horizontal skew factor to store + */ + SkMatrix& setSkewX(SkScalar v) { return this->set(kMSkewX, v); } + + /** Sets horizontal translation. + + @param v horizontal translation to store + */ + SkMatrix& setTranslateX(SkScalar v) { return this->set(kMTransX, v); } + + /** Sets vertical translation. + + @param v vertical translation to store + */ + SkMatrix& setTranslateY(SkScalar v) { return this->set(kMTransY, v); } + + /** Sets input x-axis perspective factor, which causes mapXY() to vary input x-axis values + inversely proportional to input y-axis values. + + @param v perspective factor + */ + SkMatrix& setPerspX(SkScalar v) { return this->set(kMPersp0, v); } + + /** Sets input y-axis perspective factor, which causes mapXY() to vary input y-axis values + inversely proportional to input x-axis values. + + @param v perspective factor + */ + SkMatrix& setPerspY(SkScalar v) { return this->set(kMPersp1, v); } + + /** Sets all values from parameters. Sets matrix to: + + | scaleX skewX transX | + | skewY scaleY transY | + | persp0 persp1 persp2 | + + @param scaleX horizontal scale factor to store + @param skewX horizontal skew factor to store + @param transX horizontal translation to store + @param skewY vertical skew factor to store + @param scaleY vertical scale factor to store + @param transY vertical translation to store + @param persp0 input x-axis values perspective factor to store + @param persp1 input y-axis values perspective factor to store + @param persp2 perspective scale factor to store + */ + SkMatrix& setAll(SkScalar scaleX, SkScalar skewX, SkScalar transX, + SkScalar skewY, SkScalar scaleY, SkScalar transY, + SkScalar persp0, SkScalar persp1, SkScalar persp2) { + fMat[kMScaleX] = scaleX; + fMat[kMSkewX] = skewX; + fMat[kMTransX] = transX; + fMat[kMSkewY] = skewY; + fMat[kMScaleY] = scaleY; + fMat[kMTransY] = transY; + fMat[kMPersp0] = persp0; + fMat[kMPersp1] = persp1; + fMat[kMPersp2] = persp2; + this->setTypeMask(kUnknown_Mask); + return *this; + } + + /** Copies nine scalar values contained by SkMatrix into buffer, in member value + ascending order: kMScaleX, kMSkewX, kMTransX, kMSkewY, kMScaleY, kMTransY, + kMPersp0, kMPersp1, kMPersp2. + + @param buffer storage for nine scalar values + */ + void get9(SkScalar buffer[9]) const { + memcpy(buffer, fMat, 9 * sizeof(SkScalar)); + } + + /** Sets SkMatrix to nine scalar values in buffer, in member value ascending order: + kMScaleX, kMSkewX, kMTransX, kMSkewY, kMScaleY, kMTransY, kMPersp0, kMPersp1, + kMPersp2. + + Sets matrix to: + + | buffer[0] buffer[1] buffer[2] | + | buffer[3] buffer[4] buffer[5] | + | buffer[6] buffer[7] buffer[8] | + + In the future, set9 followed by get9 may not return the same values. Since SkMatrix + maps non-homogeneous coordinates, scaling all nine values produces an equivalent + transformation, possibly improving precision. + + @param buffer nine scalar values + */ + SkMatrix& set9(const SkScalar buffer[9]); + + /** Sets SkMatrix to identity; which has no effect on mapped SkPoint. Sets SkMatrix to: + + | 1 0 0 | + | 0 1 0 | + | 0 0 1 | + + Also called setIdentity(); use the one that provides better inline + documentation. + */ + SkMatrix& reset(); + + /** Sets SkMatrix to identity; which has no effect on mapped SkPoint. Sets SkMatrix to: + + | 1 0 0 | + | 0 1 0 | + | 0 0 1 | + + Also called reset(); use the one that provides better inline + documentation. + */ + SkMatrix& setIdentity() { return this->reset(); } + + /** Sets SkMatrix to translate by (dx, dy). + + @param dx horizontal translation + @param dy vertical translation + */ + SkMatrix& setTranslate(SkScalar dx, SkScalar dy); + + /** Sets SkMatrix to translate by (v.fX, v.fY). + + @param v vector containing horizontal and vertical translation + */ + SkMatrix& setTranslate(const SkVector& v) { return this->setTranslate(v.fX, v.fY); } + + /** Sets SkMatrix to scale by sx and sy, about a pivot point at (px, py). + The pivot point is unchanged when mapped with SkMatrix. + + @param sx horizontal scale factor + @param sy vertical scale factor + @param px pivot on x-axis + @param py pivot on y-axis + */ + SkMatrix& setScale(SkScalar sx, SkScalar sy, SkScalar px, SkScalar py); + + /** Sets SkMatrix to scale by sx and sy about at pivot point at (0, 0). + + @param sx horizontal scale factor + @param sy vertical scale factor + */ + SkMatrix& setScale(SkScalar sx, SkScalar sy); + + /** Sets SkMatrix to rotate by degrees about a pivot point at (px, py). + The pivot point is unchanged when mapped with SkMatrix. + + Positive degrees rotates clockwise. + + @param degrees angle of axes relative to upright axes + @param px pivot on x-axis + @param py pivot on y-axis + */ + SkMatrix& setRotate(SkScalar degrees, SkScalar px, SkScalar py); + + /** Sets SkMatrix to rotate by degrees about a pivot point at (0, 0). + Positive degrees rotates clockwise. + + @param degrees angle of axes relative to upright axes + */ + SkMatrix& setRotate(SkScalar degrees); + + /** Sets SkMatrix to rotate by sinValue and cosValue, about a pivot point at (px, py). + The pivot point is unchanged when mapped with SkMatrix. + + Vector (sinValue, cosValue) describes the angle of rotation relative to (0, 1). + Vector length specifies scale. + + @param sinValue rotation vector x-axis component + @param cosValue rotation vector y-axis component + @param px pivot on x-axis + @param py pivot on y-axis + */ + SkMatrix& setSinCos(SkScalar sinValue, SkScalar cosValue, + SkScalar px, SkScalar py); + + /** Sets SkMatrix to rotate by sinValue and cosValue, about a pivot point at (0, 0). + + Vector (sinValue, cosValue) describes the angle of rotation relative to (0, 1). + Vector length specifies scale. + + @param sinValue rotation vector x-axis component + @param cosValue rotation vector y-axis component + */ + SkMatrix& setSinCos(SkScalar sinValue, SkScalar cosValue); + + /** Sets SkMatrix to rotate, scale, and translate using a compressed matrix form. + + Vector (rsxForm.fSSin, rsxForm.fSCos) describes the angle of rotation relative + to (0, 1). Vector length specifies scale. Mapped point is rotated and scaled + by vector, then translated by (rsxForm.fTx, rsxForm.fTy). + + @param rsxForm compressed SkRSXform matrix + @return reference to SkMatrix + + example: https://fiddle.skia.org/c/@Matrix_setRSXform + */ + SkMatrix& setRSXform(const SkRSXform& rsxForm); + + /** Sets SkMatrix to skew by kx and ky, about a pivot point at (px, py). + The pivot point is unchanged when mapped with SkMatrix. + + @param kx horizontal skew factor + @param ky vertical skew factor + @param px pivot on x-axis + @param py pivot on y-axis + */ + SkMatrix& setSkew(SkScalar kx, SkScalar ky, SkScalar px, SkScalar py); + + /** Sets SkMatrix to skew by kx and ky, about a pivot point at (0, 0). + + @param kx horizontal skew factor + @param ky vertical skew factor + */ + SkMatrix& setSkew(SkScalar kx, SkScalar ky); + + /** Sets SkMatrix to SkMatrix a multiplied by SkMatrix b. Either a or b may be this. + + Given: + + | A B C | | J K L | + a = | D E F |, b = | M N O | + | G H I | | P Q R | + + sets SkMatrix to: + + | A B C | | J K L | | AJ+BM+CP AK+BN+CQ AL+BO+CR | + a * b = | D E F | * | M N O | = | DJ+EM+FP DK+EN+FQ DL+EO+FR | + | G H I | | P Q R | | GJ+HM+IP GK+HN+IQ GL+HO+IR | + + @param a SkMatrix on left side of multiply expression + @param b SkMatrix on right side of multiply expression + */ + SkMatrix& setConcat(const SkMatrix& a, const SkMatrix& b); + + /** Sets SkMatrix to SkMatrix multiplied by SkMatrix constructed from translation (dx, dy). + This can be thought of as moving the point to be mapped before applying SkMatrix. + + Given: + + | A B C | | 1 0 dx | + Matrix = | D E F |, T(dx, dy) = | 0 1 dy | + | G H I | | 0 0 1 | + + sets SkMatrix to: + + | A B C | | 1 0 dx | | A B A*dx+B*dy+C | + Matrix * T(dx, dy) = | D E F | | 0 1 dy | = | D E D*dx+E*dy+F | + | G H I | | 0 0 1 | | G H G*dx+H*dy+I | + + @param dx x-axis translation before applying SkMatrix + @param dy y-axis translation before applying SkMatrix + */ + SkMatrix& preTranslate(SkScalar dx, SkScalar dy); + + /** Sets SkMatrix to SkMatrix multiplied by SkMatrix constructed from scaling by (sx, sy) + about pivot point (px, py). + This can be thought of as scaling about a pivot point before applying SkMatrix. + + Given: + + | A B C | | sx 0 dx | + Matrix = | D E F |, S(sx, sy, px, py) = | 0 sy dy | + | G H I | | 0 0 1 | + + where + + dx = px - sx * px + dy = py - sy * py + + sets SkMatrix to: + + | A B C | | sx 0 dx | | A*sx B*sy A*dx+B*dy+C | + Matrix * S(sx, sy, px, py) = | D E F | | 0 sy dy | = | D*sx E*sy D*dx+E*dy+F | + | G H I | | 0 0 1 | | G*sx H*sy G*dx+H*dy+I | + + @param sx horizontal scale factor + @param sy vertical scale factor + @param px pivot on x-axis + @param py pivot on y-axis + */ + SkMatrix& preScale(SkScalar sx, SkScalar sy, SkScalar px, SkScalar py); + + /** Sets SkMatrix to SkMatrix multiplied by SkMatrix constructed from scaling by (sx, sy) + about pivot point (0, 0). + This can be thought of as scaling about the origin before applying SkMatrix. + + Given: + + | A B C | | sx 0 0 | + Matrix = | D E F |, S(sx, sy) = | 0 sy 0 | + | G H I | | 0 0 1 | + + sets SkMatrix to: + + | A B C | | sx 0 0 | | A*sx B*sy C | + Matrix * S(sx, sy) = | D E F | | 0 sy 0 | = | D*sx E*sy F | + | G H I | | 0 0 1 | | G*sx H*sy I | + + @param sx horizontal scale factor + @param sy vertical scale factor + */ + SkMatrix& preScale(SkScalar sx, SkScalar sy); + + /** Sets SkMatrix to SkMatrix multiplied by SkMatrix constructed from rotating by degrees + about pivot point (px, py). + This can be thought of as rotating about a pivot point before applying SkMatrix. + + Positive degrees rotates clockwise. + + Given: + + | A B C | | c -s dx | + Matrix = | D E F |, R(degrees, px, py) = | s c dy | + | G H I | | 0 0 1 | + + where + + c = cos(degrees) + s = sin(degrees) + dx = s * py + (1 - c) * px + dy = -s * px + (1 - c) * py + + sets SkMatrix to: + + | A B C | | c -s dx | | Ac+Bs -As+Bc A*dx+B*dy+C | + Matrix * R(degrees, px, py) = | D E F | | s c dy | = | Dc+Es -Ds+Ec D*dx+E*dy+F | + | G H I | | 0 0 1 | | Gc+Hs -Gs+Hc G*dx+H*dy+I | + + @param degrees angle of axes relative to upright axes + @param px pivot on x-axis + @param py pivot on y-axis + */ + SkMatrix& preRotate(SkScalar degrees, SkScalar px, SkScalar py); + + /** Sets SkMatrix to SkMatrix multiplied by SkMatrix constructed from rotating by degrees + about pivot point (0, 0). + This can be thought of as rotating about the origin before applying SkMatrix. + + Positive degrees rotates clockwise. + + Given: + + | A B C | | c -s 0 | + Matrix = | D E F |, R(degrees, px, py) = | s c 0 | + | G H I | | 0 0 1 | + + where + + c = cos(degrees) + s = sin(degrees) + + sets SkMatrix to: + + | A B C | | c -s 0 | | Ac+Bs -As+Bc C | + Matrix * R(degrees, px, py) = | D E F | | s c 0 | = | Dc+Es -Ds+Ec F | + | G H I | | 0 0 1 | | Gc+Hs -Gs+Hc I | + + @param degrees angle of axes relative to upright axes + */ + SkMatrix& preRotate(SkScalar degrees); + + /** Sets SkMatrix to SkMatrix multiplied by SkMatrix constructed from skewing by (kx, ky) + about pivot point (px, py). + This can be thought of as skewing about a pivot point before applying SkMatrix. + + Given: + + | A B C | | 1 kx dx | + Matrix = | D E F |, K(kx, ky, px, py) = | ky 1 dy | + | G H I | | 0 0 1 | + + where + + dx = -kx * py + dy = -ky * px + + sets SkMatrix to: + + | A B C | | 1 kx dx | | A+B*ky A*kx+B A*dx+B*dy+C | + Matrix * K(kx, ky, px, py) = | D E F | | ky 1 dy | = | D+E*ky D*kx+E D*dx+E*dy+F | + | G H I | | 0 0 1 | | G+H*ky G*kx+H G*dx+H*dy+I | + + @param kx horizontal skew factor + @param ky vertical skew factor + @param px pivot on x-axis + @param py pivot on y-axis + */ + SkMatrix& preSkew(SkScalar kx, SkScalar ky, SkScalar px, SkScalar py); + + /** Sets SkMatrix to SkMatrix multiplied by SkMatrix constructed from skewing by (kx, ky) + about pivot point (0, 0). + This can be thought of as skewing about the origin before applying SkMatrix. + + Given: + + | A B C | | 1 kx 0 | + Matrix = | D E F |, K(kx, ky) = | ky 1 0 | + | G H I | | 0 0 1 | + + sets SkMatrix to: + + | A B C | | 1 kx 0 | | A+B*ky A*kx+B C | + Matrix * K(kx, ky) = | D E F | | ky 1 0 | = | D+E*ky D*kx+E F | + | G H I | | 0 0 1 | | G+H*ky G*kx+H I | + + @param kx horizontal skew factor + @param ky vertical skew factor + */ + SkMatrix& preSkew(SkScalar kx, SkScalar ky); + + /** Sets SkMatrix to SkMatrix multiplied by SkMatrix other. + This can be thought of mapping by other before applying SkMatrix. + + Given: + + | A B C | | J K L | + Matrix = | D E F |, other = | M N O | + | G H I | | P Q R | + + sets SkMatrix to: + + | A B C | | J K L | | AJ+BM+CP AK+BN+CQ AL+BO+CR | + Matrix * other = | D E F | * | M N O | = | DJ+EM+FP DK+EN+FQ DL+EO+FR | + | G H I | | P Q R | | GJ+HM+IP GK+HN+IQ GL+HO+IR | + + @param other SkMatrix on right side of multiply expression + */ + SkMatrix& preConcat(const SkMatrix& other); + + /** Sets SkMatrix to SkMatrix constructed from translation (dx, dy) multiplied by SkMatrix. + This can be thought of as moving the point to be mapped after applying SkMatrix. + + Given: + + | J K L | | 1 0 dx | + Matrix = | M N O |, T(dx, dy) = | 0 1 dy | + | P Q R | | 0 0 1 | + + sets SkMatrix to: + + | 1 0 dx | | J K L | | J+dx*P K+dx*Q L+dx*R | + T(dx, dy) * Matrix = | 0 1 dy | | M N O | = | M+dy*P N+dy*Q O+dy*R | + | 0 0 1 | | P Q R | | P Q R | + + @param dx x-axis translation after applying SkMatrix + @param dy y-axis translation after applying SkMatrix + */ + SkMatrix& postTranslate(SkScalar dx, SkScalar dy); + + /** Sets SkMatrix to SkMatrix constructed from scaling by (sx, sy) about pivot point + (px, py), multiplied by SkMatrix. + This can be thought of as scaling about a pivot point after applying SkMatrix. + + Given: + + | J K L | | sx 0 dx | + Matrix = | M N O |, S(sx, sy, px, py) = | 0 sy dy | + | P Q R | | 0 0 1 | + + where + + dx = px - sx * px + dy = py - sy * py + + sets SkMatrix to: + + | sx 0 dx | | J K L | | sx*J+dx*P sx*K+dx*Q sx*L+dx+R | + S(sx, sy, px, py) * Matrix = | 0 sy dy | | M N O | = | sy*M+dy*P sy*N+dy*Q sy*O+dy*R | + | 0 0 1 | | P Q R | | P Q R | + + @param sx horizontal scale factor + @param sy vertical scale factor + @param px pivot on x-axis + @param py pivot on y-axis + */ + SkMatrix& postScale(SkScalar sx, SkScalar sy, SkScalar px, SkScalar py); + + /** Sets SkMatrix to SkMatrix constructed from scaling by (sx, sy) about pivot point + (0, 0), multiplied by SkMatrix. + This can be thought of as scaling about the origin after applying SkMatrix. + + Given: + + | J K L | | sx 0 0 | + Matrix = | M N O |, S(sx, sy) = | 0 sy 0 | + | P Q R | | 0 0 1 | + + sets SkMatrix to: + + | sx 0 0 | | J K L | | sx*J sx*K sx*L | + S(sx, sy) * Matrix = | 0 sy 0 | | M N O | = | sy*M sy*N sy*O | + | 0 0 1 | | P Q R | | P Q R | + + @param sx horizontal scale factor + @param sy vertical scale factor + */ + SkMatrix& postScale(SkScalar sx, SkScalar sy); + + /** Sets SkMatrix to SkMatrix constructed from rotating by degrees about pivot point + (px, py), multiplied by SkMatrix. + This can be thought of as rotating about a pivot point after applying SkMatrix. + + Positive degrees rotates clockwise. + + Given: + + | J K L | | c -s dx | + Matrix = | M N O |, R(degrees, px, py) = | s c dy | + | P Q R | | 0 0 1 | + + where + + c = cos(degrees) + s = sin(degrees) + dx = s * py + (1 - c) * px + dy = -s * px + (1 - c) * py + + sets SkMatrix to: + + |c -s dx| |J K L| |cJ-sM+dx*P cK-sN+dx*Q cL-sO+dx+R| + R(degrees, px, py) * Matrix = |s c dy| |M N O| = |sJ+cM+dy*P sK+cN+dy*Q sL+cO+dy*R| + |0 0 1| |P Q R| | P Q R| + + @param degrees angle of axes relative to upright axes + @param px pivot on x-axis + @param py pivot on y-axis + */ + SkMatrix& postRotate(SkScalar degrees, SkScalar px, SkScalar py); + + /** Sets SkMatrix to SkMatrix constructed from rotating by degrees about pivot point + (0, 0), multiplied by SkMatrix. + This can be thought of as rotating about the origin after applying SkMatrix. + + Positive degrees rotates clockwise. + + Given: + + | J K L | | c -s 0 | + Matrix = | M N O |, R(degrees, px, py) = | s c 0 | + | P Q R | | 0 0 1 | + + where + + c = cos(degrees) + s = sin(degrees) + + sets SkMatrix to: + + | c -s dx | | J K L | | cJ-sM cK-sN cL-sO | + R(degrees, px, py) * Matrix = | s c dy | | M N O | = | sJ+cM sK+cN sL+cO | + | 0 0 1 | | P Q R | | P Q R | + + @param degrees angle of axes relative to upright axes + */ + SkMatrix& postRotate(SkScalar degrees); + + /** Sets SkMatrix to SkMatrix constructed from skewing by (kx, ky) about pivot point + (px, py), multiplied by SkMatrix. + This can be thought of as skewing about a pivot point after applying SkMatrix. + + Given: + + | J K L | | 1 kx dx | + Matrix = | M N O |, K(kx, ky, px, py) = | ky 1 dy | + | P Q R | | 0 0 1 | + + where + + dx = -kx * py + dy = -ky * px + + sets SkMatrix to: + + | 1 kx dx| |J K L| |J+kx*M+dx*P K+kx*N+dx*Q L+kx*O+dx+R| + K(kx, ky, px, py) * Matrix = |ky 1 dy| |M N O| = |ky*J+M+dy*P ky*K+N+dy*Q ky*L+O+dy*R| + | 0 0 1| |P Q R| | P Q R| + + @param kx horizontal skew factor + @param ky vertical skew factor + @param px pivot on x-axis + @param py pivot on y-axis + */ + SkMatrix& postSkew(SkScalar kx, SkScalar ky, SkScalar px, SkScalar py); + + /** Sets SkMatrix to SkMatrix constructed from skewing by (kx, ky) about pivot point + (0, 0), multiplied by SkMatrix. + This can be thought of as skewing about the origin after applying SkMatrix. + + Given: + + | J K L | | 1 kx 0 | + Matrix = | M N O |, K(kx, ky) = | ky 1 0 | + | P Q R | | 0 0 1 | + + sets SkMatrix to: + + | 1 kx 0 | | J K L | | J+kx*M K+kx*N L+kx*O | + K(kx, ky) * Matrix = | ky 1 0 | | M N O | = | ky*J+M ky*K+N ky*L+O | + | 0 0 1 | | P Q R | | P Q R | + + @param kx horizontal skew factor + @param ky vertical skew factor + */ + SkMatrix& postSkew(SkScalar kx, SkScalar ky); + + /** Sets SkMatrix to SkMatrix other multiplied by SkMatrix. + This can be thought of mapping by other after applying SkMatrix. + + Given: + + | J K L | | A B C | + Matrix = | M N O |, other = | D E F | + | P Q R | | G H I | + + sets SkMatrix to: + + | A B C | | J K L | | AJ+BM+CP AK+BN+CQ AL+BO+CR | + other * Matrix = | D E F | * | M N O | = | DJ+EM+FP DK+EN+FQ DL+EO+FR | + | G H I | | P Q R | | GJ+HM+IP GK+HN+IQ GL+HO+IR | + + @param other SkMatrix on left side of multiply expression + */ + SkMatrix& postConcat(const SkMatrix& other); + +#ifndef SK_SUPPORT_LEGACY_MATRIX_RECTTORECT +private: +#endif + /** Sets SkMatrix to scale and translate src SkRect to dst SkRect. stf selects whether + mapping completely fills dst or preserves the aspect ratio, and how to align + src within dst. Returns false if src is empty, and sets SkMatrix to identity. + Returns true if dst is empty, and sets SkMatrix to: + + | 0 0 0 | + | 0 0 0 | + | 0 0 1 | + + @param src SkRect to map from + @param dst SkRect to map to + @return true if SkMatrix can represent SkRect mapping + + example: https://fiddle.skia.org/c/@Matrix_setRectToRect + */ + bool setRectToRect(const SkRect& src, const SkRect& dst, ScaleToFit stf); + + /** Returns SkMatrix set to scale and translate src SkRect to dst SkRect. stf selects + whether mapping completely fills dst or preserves the aspect ratio, and how to + align src within dst. Returns the identity SkMatrix if src is empty. If dst is + empty, returns SkMatrix set to: + + | 0 0 0 | + | 0 0 0 | + | 0 0 1 | + + @param src SkRect to map from + @param dst SkRect to map to + @return SkMatrix mapping src to dst + */ + static SkMatrix MakeRectToRect(const SkRect& src, const SkRect& dst, ScaleToFit stf) { + SkMatrix m; + m.setRectToRect(src, dst, stf); + return m; + } +#ifndef SK_SUPPORT_LEGACY_MATRIX_RECTTORECT +public: +#endif + + /** Sets SkMatrix to map src to dst. count must be zero or greater, and four or less. + + If count is zero, sets SkMatrix to identity and returns true. + If count is one, sets SkMatrix to translate and returns true. + If count is two or more, sets SkMatrix to map SkPoint if possible; returns false + if SkMatrix cannot be constructed. If count is four, SkMatrix may include + perspective. + + @param src SkPoint to map from + @param dst SkPoint to map to + @param count number of SkPoint in src and dst + @return true if SkMatrix was constructed successfully + + example: https://fiddle.skia.org/c/@Matrix_setPolyToPoly + */ + bool setPolyToPoly(const SkPoint src[], const SkPoint dst[], int count); + + /** Sets inverse to reciprocal matrix, returning true if SkMatrix can be inverted. + Geometrically, if SkMatrix maps from source to destination, inverse SkMatrix + maps from destination to source. If SkMatrix can not be inverted, inverse is + unchanged. + + @param inverse storage for inverted SkMatrix; may be nullptr + @return true if SkMatrix can be inverted + */ + [[nodiscard]] bool invert(SkMatrix* inverse) const { + // Allow the trivial case to be inlined. + if (this->isIdentity()) { + if (inverse) { + inverse->reset(); + } + return true; + } + return this->invertNonIdentity(inverse); + } + + /** Fills affine with identity values in column major order. + Sets affine to: + + | 1 0 0 | + | 0 1 0 | + + Affine 3 by 2 matrices in column major order are used by OpenGL and XPS. + + @param affine storage for 3 by 2 affine matrix + + example: https://fiddle.skia.org/c/@Matrix_SetAffineIdentity + */ + static void SetAffineIdentity(SkScalar affine[6]); + + /** Fills affine in column major order. Sets affine to: + + | scale-x skew-x translate-x | + | skew-y scale-y translate-y | + + If SkMatrix contains perspective, returns false and leaves affine unchanged. + + @param affine storage for 3 by 2 affine matrix; may be nullptr + @return true if SkMatrix does not contain perspective + */ + [[nodiscard]] bool asAffine(SkScalar affine[6]) const; + + /** Sets SkMatrix to affine values, passed in column major order. Given affine, + column, then row, as: + + | scale-x skew-x translate-x | + | skew-y scale-y translate-y | + + SkMatrix is set, row, then column, to: + + | scale-x skew-x translate-x | + | skew-y scale-y translate-y | + | 0 0 1 | + + @param affine 3 by 2 affine matrix + */ + SkMatrix& setAffine(const SkScalar affine[6]); + + /** + * A matrix is categorized as 'perspective' if the bottom row is not [0, 0, 1]. + * However, for most uses (e.g. mapPoints) a bottom row of [0, 0, X] behaves like a + * non-perspective matrix, though it will be categorized as perspective. Calling + * normalizePerspective() will change the matrix such that, if its bottom row was [0, 0, X], + * it will be changed to [0, 0, 1] by scaling the rest of the matrix by 1/X. + * + * | A B C | | A/X B/X C/X | + * | D E F | -> | D/X E/X F/X | for X != 0 + * | 0 0 X | | 0 0 1 | + */ + void normalizePerspective() { + if (fMat[8] != 1) { + this->doNormalizePerspective(); + } + } + + /** Maps src SkPoint array of length count to dst SkPoint array of equal or greater + length. SkPoint are mapped by multiplying each SkPoint by SkMatrix. Given: + + | A B C | | x | + Matrix = | D E F |, pt = | y | + | G H I | | 1 | + + where + + for (i = 0; i < count; ++i) { + x = src[i].fX + y = src[i].fY + } + + each dst SkPoint is computed as: + + |A B C| |x| Ax+By+C Dx+Ey+F + Matrix * pt = |D E F| |y| = |Ax+By+C Dx+Ey+F Gx+Hy+I| = ------- , ------- + |G H I| |1| Gx+Hy+I Gx+Hy+I + + src and dst may point to the same storage. + + @param dst storage for mapped SkPoint + @param src SkPoint to transform + @param count number of SkPoint to transform + + example: https://fiddle.skia.org/c/@Matrix_mapPoints + */ + void mapPoints(SkPoint dst[], const SkPoint src[], int count) const; + + /** Maps pts SkPoint array of length count in place. SkPoint are mapped by multiplying + each SkPoint by SkMatrix. Given: + + | A B C | | x | + Matrix = | D E F |, pt = | y | + | G H I | | 1 | + + where + + for (i = 0; i < count; ++i) { + x = pts[i].fX + y = pts[i].fY + } + + each resulting pts SkPoint is computed as: + + |A B C| |x| Ax+By+C Dx+Ey+F + Matrix * pt = |D E F| |y| = |Ax+By+C Dx+Ey+F Gx+Hy+I| = ------- , ------- + |G H I| |1| Gx+Hy+I Gx+Hy+I + + @param pts storage for mapped SkPoint + @param count number of SkPoint to transform + */ + void mapPoints(SkPoint pts[], int count) const { + this->mapPoints(pts, pts, count); + } + + /** Maps src SkPoint3 array of length count to dst SkPoint3 array, which must of length count or + greater. SkPoint3 array is mapped by multiplying each SkPoint3 by SkMatrix. Given: + + | A B C | | x | + Matrix = | D E F |, src = | y | + | G H I | | z | + + each resulting dst SkPoint is computed as: + + |A B C| |x| + Matrix * src = |D E F| |y| = |Ax+By+Cz Dx+Ey+Fz Gx+Hy+Iz| + |G H I| |z| + + @param dst storage for mapped SkPoint3 array + @param src SkPoint3 array to transform + @param count items in SkPoint3 array to transform + + example: https://fiddle.skia.org/c/@Matrix_mapHomogeneousPoints + */ + void mapHomogeneousPoints(SkPoint3 dst[], const SkPoint3 src[], int count) const; + + /** + * Returns homogeneous points, starting with 2D src points (with implied w = 1). + */ + void mapHomogeneousPoints(SkPoint3 dst[], const SkPoint src[], int count) const; + + /** Returns SkPoint pt multiplied by SkMatrix. Given: + + | A B C | | x | + Matrix = | D E F |, pt = | y | + | G H I | | 1 | + + result is computed as: + + |A B C| |x| Ax+By+C Dx+Ey+F + Matrix * pt = |D E F| |y| = |Ax+By+C Dx+Ey+F Gx+Hy+I| = ------- , ------- + |G H I| |1| Gx+Hy+I Gx+Hy+I + + @param p SkPoint to map + @return mapped SkPoint + */ + SkPoint mapPoint(SkPoint pt) const { + SkPoint result; + this->mapXY(pt.x(), pt.y(), &result); + return result; + } + + /** Maps SkPoint (x, y) to result. SkPoint is mapped by multiplying by SkMatrix. Given: + + | A B C | | x | + Matrix = | D E F |, pt = | y | + | G H I | | 1 | + + result is computed as: + + |A B C| |x| Ax+By+C Dx+Ey+F + Matrix * pt = |D E F| |y| = |Ax+By+C Dx+Ey+F Gx+Hy+I| = ------- , ------- + |G H I| |1| Gx+Hy+I Gx+Hy+I + + @param x x-axis value of SkPoint to map + @param y y-axis value of SkPoint to map + @param result storage for mapped SkPoint + + example: https://fiddle.skia.org/c/@Matrix_mapXY + */ + void mapXY(SkScalar x, SkScalar y, SkPoint* result) const; + + /** Returns SkPoint (x, y) multiplied by SkMatrix. Given: + + | A B C | | x | + Matrix = | D E F |, pt = | y | + | G H I | | 1 | + + result is computed as: + + |A B C| |x| Ax+By+C Dx+Ey+F + Matrix * pt = |D E F| |y| = |Ax+By+C Dx+Ey+F Gx+Hy+I| = ------- , ------- + |G H I| |1| Gx+Hy+I Gx+Hy+I + + @param x x-axis value of SkPoint to map + @param y y-axis value of SkPoint to map + @return mapped SkPoint + */ + SkPoint mapXY(SkScalar x, SkScalar y) const { + SkPoint result; + this->mapXY(x,y, &result); + return result; + } + + + /** Returns (0, 0) multiplied by SkMatrix. Given: + + | A B C | | 0 | + Matrix = | D E F |, pt = | 0 | + | G H I | | 1 | + + result is computed as: + + |A B C| |0| C F + Matrix * pt = |D E F| |0| = |C F I| = - , - + |G H I| |1| I I + + @return mapped (0, 0) + */ + SkPoint mapOrigin() const { + SkScalar x = this->getTranslateX(), + y = this->getTranslateY(); + if (this->hasPerspective()) { + SkScalar w = fMat[kMPersp2]; + if (w) { w = 1 / w; } + x *= w; + y *= w; + } + return {x, y}; + } + + /** Maps src vector array of length count to vector SkPoint array of equal or greater + length. Vectors are mapped by multiplying each vector by SkMatrix, treating + SkMatrix translation as zero. Given: + + | A B 0 | | x | + Matrix = | D E 0 |, src = | y | + | G H I | | 1 | + + where + + for (i = 0; i < count; ++i) { + x = src[i].fX + y = src[i].fY + } + + each dst vector is computed as: + + |A B 0| |x| Ax+By Dx+Ey + Matrix * src = |D E 0| |y| = |Ax+By Dx+Ey Gx+Hy+I| = ------- , ------- + |G H I| |1| Gx+Hy+I Gx+Hy+I + + src and dst may point to the same storage. + + @param dst storage for mapped vectors + @param src vectors to transform + @param count number of vectors to transform + + example: https://fiddle.skia.org/c/@Matrix_mapVectors + */ + void mapVectors(SkVector dst[], const SkVector src[], int count) const; + + /** Maps vecs vector array of length count in place, multiplying each vector by + SkMatrix, treating SkMatrix translation as zero. Given: + + | A B 0 | | x | + Matrix = | D E 0 |, vec = | y | + | G H I | | 1 | + + where + + for (i = 0; i < count; ++i) { + x = vecs[i].fX + y = vecs[i].fY + } + + each result vector is computed as: + + |A B 0| |x| Ax+By Dx+Ey + Matrix * vec = |D E 0| |y| = |Ax+By Dx+Ey Gx+Hy+I| = ------- , ------- + |G H I| |1| Gx+Hy+I Gx+Hy+I + + @param vecs vectors to transform, and storage for mapped vectors + @param count number of vectors to transform + */ + void mapVectors(SkVector vecs[], int count) const { + this->mapVectors(vecs, vecs, count); + } + + /** Maps vector (dx, dy) to result. Vector is mapped by multiplying by SkMatrix, + treating SkMatrix translation as zero. Given: + + | A B 0 | | dx | + Matrix = | D E 0 |, vec = | dy | + | G H I | | 1 | + + each result vector is computed as: + + |A B 0| |dx| A*dx+B*dy D*dx+E*dy + Matrix * vec = |D E 0| |dy| = |A*dx+B*dy D*dx+E*dy G*dx+H*dy+I| = ----------- , ----------- + |G H I| | 1| G*dx+H*dy+I G*dx+*dHy+I + + @param dx x-axis value of vector to map + @param dy y-axis value of vector to map + @param result storage for mapped vector + */ + void mapVector(SkScalar dx, SkScalar dy, SkVector* result) const { + SkVector vec = { dx, dy }; + this->mapVectors(result, &vec, 1); + } + + /** Returns vector (dx, dy) multiplied by SkMatrix, treating SkMatrix translation as zero. + Given: + + | A B 0 | | dx | + Matrix = | D E 0 |, vec = | dy | + | G H I | | 1 | + + each result vector is computed as: + + |A B 0| |dx| A*dx+B*dy D*dx+E*dy + Matrix * vec = |D E 0| |dy| = |A*dx+B*dy D*dx+E*dy G*dx+H*dy+I| = ----------- , ----------- + |G H I| | 1| G*dx+H*dy+I G*dx+*dHy+I + + @param dx x-axis value of vector to map + @param dy y-axis value of vector to map + @return mapped vector + */ + SkVector mapVector(SkScalar dx, SkScalar dy) const { + SkVector vec = { dx, dy }; + this->mapVectors(&vec, &vec, 1); + return vec; + } + + /** Sets dst to bounds of src corners mapped by SkMatrix. + Returns true if mapped corners are dst corners. + + Returned value is the same as calling rectStaysRect(). + + @param dst storage for bounds of mapped SkPoint + @param src SkRect to map + @param pc whether to apply perspective clipping + @return true if dst is equivalent to mapped src + + example: https://fiddle.skia.org/c/@Matrix_mapRect + */ + bool mapRect(SkRect* dst, const SkRect& src, + SkApplyPerspectiveClip pc = SkApplyPerspectiveClip::kYes) const; + + /** Sets rect to bounds of rect corners mapped by SkMatrix. + Returns true if mapped corners are computed rect corners. + + Returned value is the same as calling rectStaysRect(). + + @param rect rectangle to map, and storage for bounds of mapped corners + @param pc whether to apply perspective clipping + @return true if result is equivalent to mapped rect + */ + bool mapRect(SkRect* rect, SkApplyPerspectiveClip pc = SkApplyPerspectiveClip::kYes) const { + return this->mapRect(rect, *rect, pc); + } + + /** Returns bounds of src corners mapped by SkMatrix. + + @param src rectangle to map + @return mapped bounds + */ + SkRect mapRect(const SkRect& src, + SkApplyPerspectiveClip pc = SkApplyPerspectiveClip::kYes) const { + SkRect dst; + (void)this->mapRect(&dst, src, pc); + return dst; + } + + /** Maps four corners of rect to dst. SkPoint are mapped by multiplying each + rect corner by SkMatrix. rect corner is processed in this order: + (rect.fLeft, rect.fTop), (rect.fRight, rect.fTop), (rect.fRight, rect.fBottom), + (rect.fLeft, rect.fBottom). + + rect may be empty: rect.fLeft may be greater than or equal to rect.fRight; + rect.fTop may be greater than or equal to rect.fBottom. + + Given: + + | A B C | | x | + Matrix = | D E F |, pt = | y | + | G H I | | 1 | + + where pt is initialized from each of (rect.fLeft, rect.fTop), + (rect.fRight, rect.fTop), (rect.fRight, rect.fBottom), (rect.fLeft, rect.fBottom), + each dst SkPoint is computed as: + + |A B C| |x| Ax+By+C Dx+Ey+F + Matrix * pt = |D E F| |y| = |Ax+By+C Dx+Ey+F Gx+Hy+I| = ------- , ------- + |G H I| |1| Gx+Hy+I Gx+Hy+I + + @param dst storage for mapped corner SkPoint + @param rect SkRect to map + + Note: this does not perform perspective clipping (as that might result in more than + 4 points, so results are suspect if the matrix contains perspective. + */ + void mapRectToQuad(SkPoint dst[4], const SkRect& rect) const { + // This could potentially be faster if we only transformed each x and y of the rect once. + rect.toQuad(dst); + this->mapPoints(dst, 4); + } + + /** Sets dst to bounds of src corners mapped by SkMatrix. If matrix contains + elements other than scale or translate: asserts if SK_DEBUG is defined; + otherwise, results are undefined. + + @param dst storage for bounds of mapped SkPoint + @param src SkRect to map + + example: https://fiddle.skia.org/c/@Matrix_mapRectScaleTranslate + */ + void mapRectScaleTranslate(SkRect* dst, const SkRect& src) const; + + /** Returns geometric mean radius of ellipse formed by constructing circle of + size radius, and mapping constructed circle with SkMatrix. The result squared is + equal to the major axis length times the minor axis length. + Result is not meaningful if SkMatrix contains perspective elements. + + @param radius circle size to map + @return average mapped radius + + example: https://fiddle.skia.org/c/@Matrix_mapRadius + */ + SkScalar mapRadius(SkScalar radius) const; + + /** Compares a and b; returns true if a and b are numerically equal. Returns true + even if sign of zero values are different. Returns false if either SkMatrix + contains NaN, even if the other SkMatrix also contains NaN. + + @param a SkMatrix to compare + @param b SkMatrix to compare + @return true if SkMatrix a and SkMatrix b are numerically equal + */ + friend SK_API bool operator==(const SkMatrix& a, const SkMatrix& b); + + /** Compares a and b; returns true if a and b are not numerically equal. Returns false + even if sign of zero values are different. Returns true if either SkMatrix + contains NaN, even if the other SkMatrix also contains NaN. + + @param a SkMatrix to compare + @param b SkMatrix to compare + @return true if SkMatrix a and SkMatrix b are numerically not equal + */ + friend SK_API bool operator!=(const SkMatrix& a, const SkMatrix& b) { + return !(a == b); + } + + /** Writes text representation of SkMatrix to standard output. Floating point values + are written with limited precision; it may not be possible to reconstruct + original SkMatrix from output. + + example: https://fiddle.skia.org/c/@Matrix_dump + */ + void dump() const; + + /** Returns the minimum scaling factor of SkMatrix by decomposing the scaling and + skewing elements. + Returns -1 if scale factor overflows or SkMatrix contains perspective. + + @return minimum scale factor + + example: https://fiddle.skia.org/c/@Matrix_getMinScale + */ + SkScalar getMinScale() const; + + /** Returns the maximum scaling factor of SkMatrix by decomposing the scaling and + skewing elements. + Returns -1 if scale factor overflows or SkMatrix contains perspective. + + @return maximum scale factor + + example: https://fiddle.skia.org/c/@Matrix_getMaxScale + */ + SkScalar getMaxScale() const; + + /** Sets scaleFactors[0] to the minimum scaling factor, and scaleFactors[1] to the + maximum scaling factor. Scaling factors are computed by decomposing + the SkMatrix scaling and skewing elements. + + Returns true if scaleFactors are found; otherwise, returns false and sets + scaleFactors to undefined values. + + @param scaleFactors storage for minimum and maximum scale factors + @return true if scale factors were computed correctly + */ + [[nodiscard]] bool getMinMaxScales(SkScalar scaleFactors[2]) const; + + /** Decomposes SkMatrix into scale components and whatever remains. Returns false if + SkMatrix could not be decomposed. + + Sets scale to portion of SkMatrix that scale axes. Sets remaining to SkMatrix + with scaling factored out. remaining may be passed as nullptr + to determine if SkMatrix can be decomposed without computing remainder. + + Returns true if scale components are found. scale and remaining are + unchanged if SkMatrix contains perspective; scale factors are not finite, or + are nearly zero. + + On success: Matrix = Remaining * scale. + + @param scale axes scaling factors; may be nullptr + @param remaining SkMatrix without scaling; may be nullptr + @return true if scale can be computed + + example: https://fiddle.skia.org/c/@Matrix_decomposeScale + */ + bool decomposeScale(SkSize* scale, SkMatrix* remaining = nullptr) const; + + /** Returns reference to const identity SkMatrix. Returned SkMatrix is set to: + + | 1 0 0 | + | 0 1 0 | + | 0 0 1 | + + @return const identity SkMatrix + + example: https://fiddle.skia.org/c/@Matrix_I + */ + static const SkMatrix& I(); + + /** Returns reference to a const SkMatrix with invalid values. Returned SkMatrix is set + to: + + | SK_ScalarMax SK_ScalarMax SK_ScalarMax | + | SK_ScalarMax SK_ScalarMax SK_ScalarMax | + | SK_ScalarMax SK_ScalarMax SK_ScalarMax | + + @return const invalid SkMatrix + + example: https://fiddle.skia.org/c/@Matrix_InvalidMatrix + */ + static const SkMatrix& InvalidMatrix(); + + /** Returns SkMatrix a multiplied by SkMatrix b. + + Given: + + | A B C | | J K L | + a = | D E F |, b = | M N O | + | G H I | | P Q R | + + sets SkMatrix to: + + | A B C | | J K L | | AJ+BM+CP AK+BN+CQ AL+BO+CR | + a * b = | D E F | * | M N O | = | DJ+EM+FP DK+EN+FQ DL+EO+FR | + | G H I | | P Q R | | GJ+HM+IP GK+HN+IQ GL+HO+IR | + + @param a SkMatrix on left side of multiply expression + @param b SkMatrix on right side of multiply expression + @return SkMatrix computed from a times b + */ + static SkMatrix Concat(const SkMatrix& a, const SkMatrix& b) { + SkMatrix result; + result.setConcat(a, b); + return result; + } + + friend SkMatrix operator*(const SkMatrix& a, const SkMatrix& b) { + return Concat(a, b); + } + + /** Sets internal cache to unknown state. Use to force update after repeated + modifications to SkMatrix element reference returned by operator[](int index). + */ + void dirtyMatrixTypeCache() { + this->setTypeMask(kUnknown_Mask); + } + + /** Initializes SkMatrix with scale and translate elements. + + | sx 0 tx | + | 0 sy ty | + | 0 0 1 | + + @param sx horizontal scale factor to store + @param sy vertical scale factor to store + @param tx horizontal translation to store + @param ty vertical translation to store + */ + void setScaleTranslate(SkScalar sx, SkScalar sy, SkScalar tx, SkScalar ty) { + fMat[kMScaleX] = sx; + fMat[kMSkewX] = 0; + fMat[kMTransX] = tx; + + fMat[kMSkewY] = 0; + fMat[kMScaleY] = sy; + fMat[kMTransY] = ty; + + fMat[kMPersp0] = 0; + fMat[kMPersp1] = 0; + fMat[kMPersp2] = 1; + + int mask = 0; + if (sx != 1 || sy != 1) { + mask |= kScale_Mask; + } + if (tx != 0.0f || ty != 0.0f) { + mask |= kTranslate_Mask; + } + if (sx != 0 && sy != 0) { + mask |= kRectStaysRect_Mask; + } + this->setTypeMask(mask); + } + + /** Returns true if all elements of the matrix are finite. Returns false if any + element is infinity, or NaN. + + @return true if matrix has only finite elements + */ + bool isFinite() const { return SkIsFinite(fMat, 9); } + +private: + /** Set if the matrix will map a rectangle to another rectangle. This + can be true if the matrix is scale-only, or rotates a multiple of + 90 degrees. + + This bit will be set on identity matrices + */ + static constexpr int kRectStaysRect_Mask = 0x10; + + /** Set if the perspective bit is valid even though the rest of + the matrix is Unknown. + */ + static constexpr int kOnlyPerspectiveValid_Mask = 0x40; + + static constexpr int kUnknown_Mask = 0x80; + + static constexpr int kORableMasks = kTranslate_Mask | + kScale_Mask | + kAffine_Mask | + kPerspective_Mask; + + static constexpr int kAllMasks = kTranslate_Mask | + kScale_Mask | + kAffine_Mask | + kPerspective_Mask | + kRectStaysRect_Mask; + + SkScalar fMat[9]; + mutable int32_t fTypeMask; + + constexpr SkMatrix(SkScalar sx, SkScalar kx, SkScalar tx, + SkScalar ky, SkScalar sy, SkScalar ty, + SkScalar p0, SkScalar p1, SkScalar p2, int typeMask) + : fMat{sx, kx, tx, + ky, sy, ty, + p0, p1, p2} + , fTypeMask(typeMask) {} + + static void ComputeInv(SkScalar dst[9], const SkScalar src[9], double invDet, bool isPersp); + + uint8_t computeTypeMask() const; + uint8_t computePerspectiveTypeMask() const; + + void setTypeMask(int mask) { + // allow kUnknown or a valid mask + SkASSERT(kUnknown_Mask == mask || (mask & kAllMasks) == mask || + ((kUnknown_Mask | kOnlyPerspectiveValid_Mask) & mask) + == (kUnknown_Mask | kOnlyPerspectiveValid_Mask)); + fTypeMask = mask; + } + + void orTypeMask(int mask) { + SkASSERT((mask & kORableMasks) == mask); + fTypeMask |= mask; + } + + void clearTypeMask(int mask) { + // only allow a valid mask + SkASSERT((mask & kAllMasks) == mask); + fTypeMask &= ~mask; + } + + TypeMask getPerspectiveTypeMaskOnly() const { + if ((fTypeMask & kUnknown_Mask) && + !(fTypeMask & kOnlyPerspectiveValid_Mask)) { + fTypeMask = this->computePerspectiveTypeMask(); + } + return (TypeMask)(fTypeMask & 0xF); + } + + /** Returns true if we already know that the matrix is identity; + false otherwise. + */ + bool isTriviallyIdentity() const { + if (fTypeMask & kUnknown_Mask) { + return false; + } + return ((fTypeMask & 0xF) == 0); + } + + inline void updateTranslateMask() { + if ((fMat[kMTransX] != 0) | (fMat[kMTransY] != 0)) { + fTypeMask |= kTranslate_Mask; + } else { + fTypeMask &= ~kTranslate_Mask; + } + } + + typedef void (*MapXYProc)(const SkMatrix& mat, SkScalar x, SkScalar y, + SkPoint* result); + + static MapXYProc GetMapXYProc(TypeMask mask) { + SkASSERT((mask & ~kAllMasks) == 0); + return gMapXYProcs[mask & kAllMasks]; + } + + MapXYProc getMapXYProc() const { + return GetMapXYProc(this->getType()); + } + + typedef void (*MapPtsProc)(const SkMatrix& mat, SkPoint dst[], + const SkPoint src[], int count); + + static MapPtsProc GetMapPtsProc(TypeMask mask) { + SkASSERT((mask & ~kAllMasks) == 0); + return gMapPtsProcs[mask & kAllMasks]; + } + + MapPtsProc getMapPtsProc() const { + return GetMapPtsProc(this->getType()); + } + + [[nodiscard]] bool invertNonIdentity(SkMatrix* inverse) const; + + static bool Poly2Proc(const SkPoint[], SkMatrix*); + static bool Poly3Proc(const SkPoint[], SkMatrix*); + static bool Poly4Proc(const SkPoint[], SkMatrix*); + + static void Identity_xy(const SkMatrix&, SkScalar, SkScalar, SkPoint*); + static void Trans_xy(const SkMatrix&, SkScalar, SkScalar, SkPoint*); + static void Scale_xy(const SkMatrix&, SkScalar, SkScalar, SkPoint*); + static void ScaleTrans_xy(const SkMatrix&, SkScalar, SkScalar, SkPoint*); + static void Rot_xy(const SkMatrix&, SkScalar, SkScalar, SkPoint*); + static void RotTrans_xy(const SkMatrix&, SkScalar, SkScalar, SkPoint*); + static void Persp_xy(const SkMatrix&, SkScalar, SkScalar, SkPoint*); + + static const MapXYProc gMapXYProcs[]; + + static void Identity_pts(const SkMatrix&, SkPoint[], const SkPoint[], int); + static void Trans_pts(const SkMatrix&, SkPoint dst[], const SkPoint[], int); + static void Scale_pts(const SkMatrix&, SkPoint dst[], const SkPoint[], int); + static void ScaleTrans_pts(const SkMatrix&, SkPoint dst[], const SkPoint[], + int count); + static void Persp_pts(const SkMatrix&, SkPoint dst[], const SkPoint[], int); + + static void Affine_vpts(const SkMatrix&, SkPoint dst[], const SkPoint[], int); + + static const MapPtsProc gMapPtsProcs[]; + + // return the number of bytes written, whether or not buffer is null + size_t writeToMemory(void* buffer) const; + /** + * Reads data from the buffer parameter + * + * @param buffer Memory to read from + * @param length Amount of memory available in the buffer + * @return number of bytes read (must be a multiple of 4) or + * 0 if there was not enough memory available + */ + size_t readFromMemory(const void* buffer, size_t length); + + // legacy method -- still needed? why not just postScale(1/divx, ...)? + bool postIDiv(int divx, int divy); + void doNormalizePerspective(); + + friend class SkPerspIter; + friend class SkMatrixPriv; + friend class SerializationTest; +}; +SK_END_REQUIRE_DENSE + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMesh.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMesh.h new file mode 100644 index 00000000000..6eccc937fd2 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMesh.h @@ -0,0 +1,429 @@ +/* + * Copyright 2021 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkMesh_DEFINED +#define SkMesh_DEFINED + +#include "include/core/SkData.h" +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkSpan.h" +#include "include/core/SkString.h" +#include "include/effects/SkRuntimeEffect.h" +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkTArray.h" + +#include +#include +#include +#include +#include +#include + +class GrDirectContext; +class SkColorSpace; +enum SkAlphaType : int; + +namespace SkSL { struct Program; } + +/** + * A specification for custom meshes. Specifies the vertex buffer attributes and stride, the + * vertex program that produces a user-defined set of varyings, and a fragment program that ingests + * the interpolated varyings and produces local coordinates for shading and optionally a color. + * + * The varyings must include a float2 named "position". If the passed varyings does not + * contain such a varying then one is implicitly added to the final specification and the SkSL + * Varyings struct described below. It is an error to have a varying named "position" that has a + * type other than float2. + * + * The provided attributes and varyings are used to create Attributes and Varyings structs in SkSL + * that are used by the shaders. Each attribute from the Attribute span becomes a member of the + * SkSL Attributes struct and likewise for the varyings. + * + * The signature of the vertex program must be: + * Varyings main(const Attributes). + * + * The signature of the fragment program must be either: + * float2 main(const Varyings) + * or + * float2 main(const Varyings, out (half4|float4) color) + * + * where the return value is the local coordinates that will be used to access SkShader. If the + * color variant is used, the returned color will be blended with SkPaint's SkShader (or SkPaint + * color in absence of a SkShader) using the SkBlender passed to SkCanvas drawMesh(). To use + * interpolated local space positions as the shader coordinates, equivalent to how SkPaths are + * shaded, return the position field from the Varying struct as the coordinates. + * + * The vertex and fragment programs may both contain uniforms. Uniforms with the same name are + * assumed to be shared between stages. It is an error to specify uniforms in the vertex and + * fragment program with the same name but different types, dimensionality, or layouts. + */ +class SK_API SkMeshSpecification : public SkNVRefCnt { +public: + /** These values are enforced when creating a specification. */ + static constexpr size_t kMaxStride = 1024; + static constexpr size_t kMaxAttributes = 8; + static constexpr size_t kStrideAlignment = 4; + static constexpr size_t kOffsetAlignment = 4; + static constexpr size_t kMaxVaryings = 6; + + struct Attribute { + enum class Type : uint32_t { // CPU representation Shader Type + kFloat, // float float + kFloat2, // two floats float2 + kFloat3, // three floats float3 + kFloat4, // four floats float4 + kUByte4_unorm, // four bytes half4 + + kLast = kUByte4_unorm + }; + Type type; + size_t offset; + SkString name; + }; + + struct Varying { + enum class Type : uint32_t { + kFloat, // "float" + kFloat2, // "float2" + kFloat3, // "float3" + kFloat4, // "float4" + kHalf, // "half" + kHalf2, // "half2" + kHalf3, // "half3" + kHalf4, // "half4" + + kLast = kHalf4 + }; + Type type; + SkString name; + }; + + using Uniform = SkRuntimeEffect::Uniform; + using Child = SkRuntimeEffect::Child; + + ~SkMeshSpecification(); + + struct Result { + sk_sp specification; + SkString error; + }; + + /** + * If successful the return is a specification and an empty error string. Otherwise, it is a + * null specification a non-empty error string. + * + * @param attributes The vertex attributes that will be consumed by 'vs'. Attributes need + * not be tightly packed but attribute offsets must be aligned to + * kOffsetAlignment and offset + size may not be greater than + * 'vertexStride'. At least one attribute is required. + * @param vertexStride The offset between successive attribute values. This must be aligned to + * kStrideAlignment. + * @param varyings The varyings that will be written by 'vs' and read by 'fs'. This may + * be empty. + * @param vs The vertex shader code that computes a vertex position and the varyings + * from the attributes. + * @param fs The fragment code that computes a local coordinate and optionally a + * color from the varyings. The local coordinate is used to sample + * SkShader. + * @param cs The colorspace of the color produced by 'fs'. Ignored if 'fs's main() + * function does not have a color out param. + * @param at The alpha type of the color produced by 'fs'. Ignored if 'fs's main() + * function does not have a color out param. Cannot be kUnknown. + */ + static Result Make(SkSpan attributes, + size_t vertexStride, + SkSpan varyings, + const SkString& vs, + const SkString& fs); + static Result Make(SkSpan attributes, + size_t vertexStride, + SkSpan varyings, + const SkString& vs, + const SkString& fs, + sk_sp cs); + static Result Make(SkSpan attributes, + size_t vertexStride, + SkSpan varyings, + const SkString& vs, + const SkString& fs, + sk_sp cs, + SkAlphaType at); + + SkSpan attributes() const { return SkSpan(fAttributes); } + + /** + * Combined size of all 'uniform' variables. When creating a SkMesh with this specification + * provide an SkData of this size, containing values for all of those variables. Use uniforms() + * to get the offset of each uniform within the SkData. + */ + size_t uniformSize() const; + + /** + * Provides info about individual uniforms including the offset into an SkData where each + * uniform value should be placed. + */ + SkSpan uniforms() const { return SkSpan(fUniforms); } + + /** Provides basic info about individual children: names, indices and runtime effect type. */ + SkSpan children() const { return SkSpan(fChildren); } + + /** Returns a pointer to the named child's description, or nullptr if not found. */ + const Child* findChild(std::string_view name) const; + + /** Returns a pointer to the named uniform variable's description, or nullptr if not found. */ + const Uniform* findUniform(std::string_view name) const; + + /** Returns a pointer to the named attribute, or nullptr if not found. */ + const Attribute* findAttribute(std::string_view name) const; + + /** Returns a pointer to the named varying, or nullptr if not found. */ + const Varying* findVarying(std::string_view name) const; + + size_t stride() const { return fStride; } + + SkColorSpace* colorSpace() const { return fColorSpace.get(); } + +private: + friend struct SkMeshSpecificationPriv; + + enum class ColorType { + kNone, + kHalf4, + kFloat4, + }; + + static Result MakeFromSourceWithStructs(SkSpan attributes, + size_t stride, + SkSpan varyings, + const SkString& vs, + const SkString& fs, + sk_sp cs, + SkAlphaType at); + + SkMeshSpecification(SkSpan, + size_t, + SkSpan, + int passthroughLocalCoordsVaryingIndex, + uint32_t deadVaryingMask, + std::vector uniforms, + std::vector children, + std::unique_ptr, + std::unique_ptr, + ColorType, + sk_sp, + SkAlphaType); + + SkMeshSpecification(const SkMeshSpecification&) = delete; + SkMeshSpecification(SkMeshSpecification&&) = delete; + + SkMeshSpecification& operator=(const SkMeshSpecification&) = delete; + SkMeshSpecification& operator=(SkMeshSpecification&&) = delete; + + const std::vector fAttributes; + const std::vector fVaryings; + const std::vector fUniforms; + const std::vector fChildren; + const std::unique_ptr fVS; + const std::unique_ptr fFS; + const size_t fStride; + uint32_t fHash; + const int fPassthroughLocalCoordsVaryingIndex; + const uint32_t fDeadVaryingMask; + const ColorType fColorType; + const sk_sp fColorSpace; + const SkAlphaType fAlphaType; +}; + +/** + * A vertex buffer, a topology, optionally an index buffer, and a compatible SkMeshSpecification. + * + * The data in the vertex buffer is expected to contain the attributes described by the spec + * for vertexCount vertices, beginning at vertexOffset. vertexOffset must be aligned to the + * SkMeshSpecification's vertex stride. The size of the buffer must be at least vertexOffset + + * spec->stride()*vertexCount (even if vertex attributes contains pad at the end of the stride). If + * the specified bounds do not contain all the points output by the spec's vertex program when + * applied to the vertices in the custom mesh, then the result is undefined. + * + * MakeIndexed may be used to create an indexed mesh. indexCount indices are read from the index + * buffer at the specified offset, which must be aligned to 2. The indices are always unsigned + * 16-bit integers. The index count must be at least 3. + * + * If Make() is used, the implicit index sequence is 0, 1, 2, 3, ... and vertexCount must be at + * least 3. + * + * Both Make() and MakeIndexed() take a SkData with the uniform values. See + * SkMeshSpecification::uniformSize() and SkMeshSpecification::uniforms() for sizing and packing + * uniforms into the SkData. + */ +class SK_API SkMesh { +public: + class IndexBuffer : public SkRefCnt { + public: + virtual size_t size() const = 0; + + /** + * Modifies the data in the IndexBuffer by copying size bytes from data into the buffer + * at offset. Fails if offset + size > this->size() or if either offset or size is not + * aligned to 4 bytes. The GrDirectContext* must match that used to create the buffer. We + * take it as a parameter to emphasize that the context must be used to update the data and + * thus the context must be valid for the current thread. + */ + bool update(GrDirectContext*, const void* data, size_t offset, size_t size); + + private: + virtual bool onUpdate(GrDirectContext*, const void* data, size_t offset, size_t size) = 0; + }; + + class VertexBuffer : public SkRefCnt { + public: + virtual size_t size() const = 0; + + /** + * Modifies the data in the IndexBuffer by copying size bytes from data into the buffer + * at offset. Fails if offset + size > this->size() or if either offset or size is not + * aligned to 4 bytes. The GrDirectContext* must match that used to create the buffer. We + * take it as a parameter to emphasize that the context must be used to update the data and + * thus the context must be valid for the current thread. + */ + bool update(GrDirectContext*, const void* data, size_t offset, size_t size); + + private: + virtual bool onUpdate(GrDirectContext*, const void* data, size_t offset, size_t size) = 0; + }; + + SkMesh(); + ~SkMesh(); + + SkMesh(const SkMesh&); + SkMesh(SkMesh&&); + + SkMesh& operator=(const SkMesh&); + SkMesh& operator=(SkMesh&&); + + enum class Mode { kTriangles, kTriangleStrip }; + + struct Result; + + using ChildPtr = SkRuntimeEffect::ChildPtr; + + /** + * Creates a non-indexed SkMesh. The returned SkMesh can be tested for validity using + * SkMesh::isValid(). An invalid mesh simply fails to draws if passed to SkCanvas::drawMesh(). + * If the mesh is invalid the returned string give contain the reason for the failure (e.g. the + * vertex buffer was null or uniform data too small). + */ + static Result Make(sk_sp, + Mode, + sk_sp, + size_t vertexCount, + size_t vertexOffset, + sk_sp uniforms, + SkSpan children, + const SkRect& bounds); + + /** + * Creates an indexed SkMesh. The returned SkMesh can be tested for validity using + * SkMesh::isValid(). A invalid mesh simply fails to draw if passed to SkCanvas::drawMesh(). + * If the mesh is invalid the returned string give contain the reason for the failure (e.g. the + * index buffer was null or uniform data too small). + */ + static Result MakeIndexed(sk_sp, + Mode, + sk_sp, + size_t vertexCount, + size_t vertexOffset, + sk_sp, + size_t indexCount, + size_t indexOffset, + sk_sp uniforms, + SkSpan children, + const SkRect& bounds); + + sk_sp refSpec() const { return fSpec; } + SkMeshSpecification* spec() const { return fSpec.get(); } + + Mode mode() const { return fMode; } + + sk_sp refVertexBuffer() const { return fVB; } + VertexBuffer* vertexBuffer() const { return fVB.get(); } + + size_t vertexOffset() const { return fVOffset; } + size_t vertexCount() const { return fVCount; } + + sk_sp refIndexBuffer() const { return fIB; } + IndexBuffer* indexBuffer() const { return fIB.get(); } + + size_t indexOffset() const { return fIOffset; } + size_t indexCount() const { return fICount; } + + sk_sp refUniforms() const { return fUniforms; } + const SkData* uniforms() const { return fUniforms.get(); } + + SkSpan children() const { return SkSpan(fChildren); } + + SkRect bounds() const { return fBounds; } + + bool isValid() const; + +private: + std::tuple validate() const; + + sk_sp fSpec; + + sk_sp fVB; + sk_sp fIB; + + sk_sp fUniforms; + skia_private::STArray<2, ChildPtr> fChildren; + + size_t fVOffset = 0; // Must be a multiple of spec->stride() + size_t fVCount = 0; + + size_t fIOffset = 0; // Must be a multiple of sizeof(uint16_t) + size_t fICount = 0; + + Mode fMode = Mode::kTriangles; + + SkRect fBounds = SkRect::MakeEmpty(); +}; + +struct SkMesh::Result { SkMesh mesh; SkString error; }; + +namespace SkMeshes { +/** + * Makes a CPU-backed index buffer to be used with SkMeshes. + * + * @param data The data used to populate the buffer, or nullptr to create a zero- + * initialized buffer. + * @param size Both the size of the data in 'data' and the size of the resulting + * buffer, in bytes. + */ +SK_API sk_sp MakeIndexBuffer(const void* data, size_t size); + +/** + * Makes a copy of an index buffer. The copy will be CPU-backed. + */ +SK_API sk_sp CopyIndexBuffer(const sk_sp&); + +/** + * Makes a CPU-backed vertex buffer to be used with SkMeshes. + * + * @param data The data used to populate the buffer, or nullptr to create a zero- + * initialized buffer. + * @param size Both the size of the data in 'data' and the size of the resulting + * buffer, in bytes. + */ +SK_API sk_sp MakeVertexBuffer(const void*, size_t size); + +/** + * Makes a copy of a vertex buffer. The copy will be CPU-backed. + */ +SK_API sk_sp CopyVertexBuffer(const sk_sp&); +} // namespace SkMeshes + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMilestone.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMilestone.h new file mode 100644 index 00000000000..05671302e55 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkMilestone.h @@ -0,0 +1,9 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SK_MILESTONE +#define SK_MILESTONE 127 +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkOpenTypeSVGDecoder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkOpenTypeSVGDecoder.h new file mode 100644 index 00000000000..5a2e48a9df1 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkOpenTypeSVGDecoder.h @@ -0,0 +1,30 @@ +/* + * Copyright 2022 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkOpenTypeSVGDecoder_DEFINED +#define SkOpenTypeSVGDecoder_DEFINED + +#include "include/core/SkColor.h" +#include "include/core/SkSpan.h" +#include "include/core/SkTypes.h" + +#include + +class SkCanvas; + +class SkOpenTypeSVGDecoder { +public: + /** Each instance probably owns an SVG DOM. + * The instance may be cached so needs to report how much memory it retains. + */ + virtual size_t approximateSize() = 0; + virtual bool render(SkCanvas&, int upem, SkGlyphID glyphId, + SkColor foregroundColor, SkSpan palette) = 0; + virtual ~SkOpenTypeSVGDecoder() = default; +}; + +#endif // SkOpenTypeSVGDecoder_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkOverdrawCanvas.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkOverdrawCanvas.h new file mode 100644 index 00000000000..5dea52ae697 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkOverdrawCanvas.h @@ -0,0 +1,94 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkOverdrawCanvas_DEFINED +#define SkOverdrawCanvas_DEFINED + +#include "include/core/SkCanvas.h" +#include "include/core/SkCanvasVirtualEnforcer.h" +#include "include/core/SkColor.h" +#include "include/core/SkPaint.h" +#include "include/core/SkSamplingOptions.h" +#include "include/core/SkScalar.h" +#include "include/private/base/SkAPI.h" +#include "include/utils/SkNWayCanvas.h" + +#include + +class SkData; +class SkDrawable; +class SkImage; +class SkMatrix; +class SkPath; +class SkPicture; +class SkRRect; +class SkRegion; +class SkTextBlob; +class SkVertices; +enum class SkBlendMode; +namespace sktext { class GlyphRunList; } +struct SkDrawShadowRec; +struct SkPoint; +struct SkRSXform; +struct SkRect; + +/** + * Captures all drawing commands. Rather than draw the actual content, this device + * increments the alpha channel of each pixel every time it would have been touched + * by a draw call. This is useful for detecting overdraw. + */ +class SK_API SkOverdrawCanvas : public SkCanvasVirtualEnforcer { +public: + /* Does not take ownership of canvas */ + SkOverdrawCanvas(SkCanvas*); + + void onDrawTextBlob(const SkTextBlob*, SkScalar, SkScalar, const SkPaint&) override; + void onDrawGlyphRunList( + const sktext::GlyphRunList& glyphRunList, const SkPaint& paint) override; + void onDrawPatch(const SkPoint[12], const SkColor[4], const SkPoint[4], SkBlendMode, + const SkPaint&) override; + void onDrawPaint(const SkPaint&) override; + void onDrawBehind(const SkPaint& paint) override; + void onDrawRect(const SkRect&, const SkPaint&) override; + void onDrawRegion(const SkRegion&, const SkPaint&) override; + void onDrawOval(const SkRect&, const SkPaint&) override; + void onDrawArc(const SkRect&, SkScalar, SkScalar, bool, const SkPaint&) override; + void onDrawDRRect(const SkRRect&, const SkRRect&, const SkPaint&) override; + void onDrawRRect(const SkRRect&, const SkPaint&) override; + void onDrawPoints(PointMode, size_t, const SkPoint[], const SkPaint&) override; + void onDrawVerticesObject(const SkVertices*, SkBlendMode, const SkPaint&) override; + void onDrawPath(const SkPath&, const SkPaint&) override; + + void onDrawImage2(const SkImage*, SkScalar, SkScalar, const SkSamplingOptions&, + const SkPaint*) override; + void onDrawImageRect2(const SkImage*, const SkRect&, const SkRect&, const SkSamplingOptions&, + const SkPaint*, SrcRectConstraint) override; + void onDrawImageLattice2(const SkImage*, const Lattice&, const SkRect&, SkFilterMode, + const SkPaint*) override; + void onDrawAtlas2(const SkImage*, const SkRSXform[], const SkRect[], const SkColor[], int, + SkBlendMode, const SkSamplingOptions&, const SkRect*, const SkPaint*) override; + + void onDrawDrawable(SkDrawable*, const SkMatrix*) override; + void onDrawPicture(const SkPicture*, const SkMatrix*, const SkPaint*) override; + + void onDrawAnnotation(const SkRect&, const char key[], SkData* value) override; + void onDrawShadowRec(const SkPath&, const SkDrawShadowRec&) override; + + void onDrawEdgeAAQuad(const SkRect&, const SkPoint[4], SkCanvas::QuadAAFlags, const SkColor4f&, + SkBlendMode) override; + void onDrawEdgeAAImageSet2(const ImageSetEntry[], int count, const SkPoint[], const SkMatrix[], + const SkSamplingOptions&,const SkPaint*, SrcRectConstraint) override; + +private: + inline SkPaint overdrawPaint(const SkPaint& paint); + + SkPaint fPaint; + + using INHERITED = SkCanvasVirtualEnforcer; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPaint.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPaint.h new file mode 100644 index 00000000000..300f0ea0884 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPaint.h @@ -0,0 +1,695 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPaint_DEFINED +#define SkPaint_DEFINED + +#include "include/core/SkColor.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkCPUTypes.h" +#include "include/private/base/SkFloatingPoint.h" +#include "include/private/base/SkTo.h" +#include "include/private/base/SkTypeTraits.h" + +#include +#include +#include + +class SkBlender; +class SkColorFilter; +class SkColorSpace; +class SkImageFilter; +class SkMaskFilter; +class SkPathEffect; +class SkShader; +enum class SkBlendMode; +struct SkRect; + +/** \class SkPaint + SkPaint controls options applied when drawing. SkPaint collects all + options outside of the SkCanvas clip and SkCanvas matrix. + + Various options apply to strokes and fills, and images. + + SkPaint collects effects and filters that describe single-pass and multiple-pass + algorithms that alter the drawing geometry, color, and transparency. For instance, + SkPaint does not directly implement dashing or blur, but contains the objects that do so. +*/ +class SK_API SkPaint { +public: + + /** Constructs SkPaint with default values. + + @return default initialized SkPaint + + example: https://fiddle.skia.org/c/@Paint_empty_constructor + */ + SkPaint(); + + /** Constructs SkPaint with default values and the given color. + + Sets alpha and RGB used when stroking and filling. The color is four floating + point values, unpremultiplied. The color values are interpreted as being in + the colorSpace. If colorSpace is nullptr, then color is assumed to be in the + sRGB color space. + + @param color unpremultiplied RGBA + @param colorSpace SkColorSpace describing the encoding of color + @return SkPaint with the given color + */ + explicit SkPaint(const SkColor4f& color, SkColorSpace* colorSpace = nullptr); + + /** Makes a shallow copy of SkPaint. SkPathEffect, SkShader, + SkMaskFilter, SkColorFilter, and SkImageFilter are shared + between the original paint and the copy. Objects containing SkRefCnt increment + their references by one. + + The referenced objects SkPathEffect, SkShader, SkMaskFilter, SkColorFilter, + and SkImageFilter cannot be modified after they are created. + This prevents objects with SkRefCnt from being modified once SkPaint refers to them. + + @param paint original to copy + @return shallow copy of paint + + example: https://fiddle.skia.org/c/@Paint_copy_const_SkPaint + */ + SkPaint(const SkPaint& paint); + + /** Implements a move constructor to avoid increasing the reference counts + of objects referenced by the paint. + + After the call, paint is undefined, and can be safely destructed. + + @param paint original to move + @return content of paint + + example: https://fiddle.skia.org/c/@Paint_move_SkPaint + */ + SkPaint(SkPaint&& paint); + + /** Decreases SkPaint SkRefCnt of owned objects: SkPathEffect, SkShader, + SkMaskFilter, SkColorFilter, and SkImageFilter. If the + objects containing SkRefCnt go to zero, they are deleted. + */ + ~SkPaint(); + + /** Makes a shallow copy of SkPaint. SkPathEffect, SkShader, + SkMaskFilter, SkColorFilter, and SkImageFilter are shared + between the original paint and the copy. Objects containing SkRefCnt in the + prior destination are decreased by one, and the referenced objects are deleted if the + resulting count is zero. Objects containing SkRefCnt in the parameter paint + are increased by one. paint is unmodified. + + @param paint original to copy + @return content of paint + + example: https://fiddle.skia.org/c/@Paint_copy_operator + */ + SkPaint& operator=(const SkPaint& paint); + + /** Moves the paint to avoid increasing the reference counts + of objects referenced by the paint parameter. Objects containing SkRefCnt in the + prior destination are decreased by one; those objects are deleted if the resulting count + is zero. + + After the call, paint is undefined, and can be safely destructed. + + @param paint original to move + @return content of paint + + example: https://fiddle.skia.org/c/@Paint_move_operator + */ + SkPaint& operator=(SkPaint&& paint); + + /** Compares a and b, and returns true if a and b are equivalent. May return false + if SkPathEffect, SkShader, SkMaskFilter, SkColorFilter, + or SkImageFilter have identical contents but different pointers. + + @param a SkPaint to compare + @param b SkPaint to compare + @return true if SkPaint pair are equivalent + */ + SK_API friend bool operator==(const SkPaint& a, const SkPaint& b); + + /** Compares a and b, and returns true if a and b are not equivalent. May return true + if SkPathEffect, SkShader, SkMaskFilter, SkColorFilter, + or SkImageFilter have identical contents but different pointers. + + @param a SkPaint to compare + @param b SkPaint to compare + @return true if SkPaint pair are not equivalent + */ + friend bool operator!=(const SkPaint& a, const SkPaint& b) { + return !(a == b); + } + + /** Sets all SkPaint contents to their initial values. This is equivalent to replacing + SkPaint with the result of SkPaint(). + + example: https://fiddle.skia.org/c/@Paint_reset + */ + void reset(); + + /** Returns true if pixels on the active edges of SkPath may be drawn with partial transparency. + @return antialiasing state + */ + bool isAntiAlias() const { + return SkToBool(fBitfields.fAntiAlias); + } + + /** Requests, but does not require, that edge pixels draw opaque or with + partial transparency. + @param aa setting for antialiasing + */ + void setAntiAlias(bool aa) { fBitfields.fAntiAlias = static_cast(aa); } + + /** Returns true if color error may be distributed to smooth color transition. + @return dithering state + */ + bool isDither() const { + return SkToBool(fBitfields.fDither); + } + + /** Requests, but does not require, to distribute color error. + @param dither setting for ditering + */ + void setDither(bool dither) { fBitfields.fDither = static_cast(dither); } + + /** \enum SkPaint::Style + Set Style to fill, stroke, or both fill and stroke geometry. + The stroke and fill + share all paint attributes; for instance, they are drawn with the same color. + + Use kStrokeAndFill_Style to avoid hitting the same pixels twice with a stroke draw and + a fill draw. + */ + enum Style : uint8_t { + kFill_Style, //!< set to fill geometry + kStroke_Style, //!< set to stroke geometry + kStrokeAndFill_Style, //!< sets to stroke and fill geometry + }; + + /** May be used to verify that SkPaint::Style is a legal value. + */ + static constexpr int kStyleCount = kStrokeAndFill_Style + 1; + + /** Returns whether the geometry is filled, stroked, or filled and stroked. + */ + Style getStyle() const { return (Style)fBitfields.fStyle; } + + /** Sets whether the geometry is filled, stroked, or filled and stroked. + Has no effect if style is not a legal SkPaint::Style value. + + example: https://fiddle.skia.org/c/@Paint_setStyle + example: https://fiddle.skia.org/c/@Stroke_Width + */ + void setStyle(Style style); + + /** + * Set paint's style to kStroke if true, or kFill if false. + */ + void setStroke(bool); + + /** Retrieves alpha and RGB, unpremultiplied, packed into 32 bits. + Use helpers SkColorGetA(), SkColorGetR(), SkColorGetG(), and SkColorGetB() to extract + a color component. + + @return unpremultiplied ARGB + */ + SkColor getColor() const { return fColor4f.toSkColor(); } + + /** Retrieves alpha and RGB, unpremultiplied, as four floating point values. RGB are + extended sRGB values (sRGB gamut, and encoded with the sRGB transfer function). + + @return unpremultiplied RGBA + */ + SkColor4f getColor4f() const { return fColor4f; } + + /** Sets alpha and RGB used when stroking and filling. The color is a 32-bit value, + unpremultiplied, packing 8-bit components for alpha, red, blue, and green. + + @param color unpremultiplied ARGB + + example: https://fiddle.skia.org/c/@Paint_setColor + */ + void setColor(SkColor color); + + /** Sets alpha and RGB used when stroking and filling. The color is four floating + point values, unpremultiplied. The color values are interpreted as being in + the colorSpace. If colorSpace is nullptr, then color is assumed to be in the + sRGB color space. + + @param color unpremultiplied RGBA + @param colorSpace SkColorSpace describing the encoding of color + */ + void setColor(const SkColor4f& color, SkColorSpace* colorSpace = nullptr); + + void setColor4f(const SkColor4f& color, SkColorSpace* colorSpace = nullptr) { + this->setColor(color, colorSpace); + } + + /** Retrieves alpha from the color used when stroking and filling. + + @return alpha ranging from zero, fully transparent, to one, fully opaque + */ + float getAlphaf() const { return fColor4f.fA; } + + // Helper that scales the alpha by 255. + uint8_t getAlpha() const { + return static_cast(sk_float_round2int(this->getAlphaf() * 255)); + } + + /** Replaces alpha, leaving RGB + unchanged. An out of range value triggers an assert in the debug + build. a is a value from 0.0 to 1.0. + a set to zero makes color fully transparent; a set to 1.0 makes color + fully opaque. + + @param a alpha component of color + */ + void setAlphaf(float a); + + // Helper that accepts an int between 0 and 255, and divides it by 255.0 + void setAlpha(U8CPU a) { + this->setAlphaf(a * (1.0f / 255)); + } + + /** Sets color used when drawing solid fills. The color components range from 0 to 255. + The color is unpremultiplied; alpha sets the transparency independent of RGB. + + @param a amount of alpha, from fully transparent (0) to fully opaque (255) + @param r amount of red, from no red (0) to full red (255) + @param g amount of green, from no green (0) to full green (255) + @param b amount of blue, from no blue (0) to full blue (255) + + example: https://fiddle.skia.org/c/@Paint_setARGB + */ + void setARGB(U8CPU a, U8CPU r, U8CPU g, U8CPU b); + + /** Returns the thickness of the pen used by SkPaint to + outline the shape. + + @return zero for hairline, greater than zero for pen thickness + */ + SkScalar getStrokeWidth() const { return fWidth; } + + /** Sets the thickness of the pen used by the paint to outline the shape. + A stroke-width of zero is treated as "hairline" width. Hairlines are always exactly one + pixel wide in device space (their thickness does not change as the canvas is scaled). + Negative stroke-widths are invalid; setting a negative width will have no effect. + + @param width zero thickness for hairline; greater than zero for pen thickness + + example: https://fiddle.skia.org/c/@Miter_Limit + example: https://fiddle.skia.org/c/@Paint_setStrokeWidth + */ + void setStrokeWidth(SkScalar width); + + /** Returns the limit at which a sharp corner is drawn beveled. + + @return zero and greater miter limit + */ + SkScalar getStrokeMiter() const { return fMiterLimit; } + + /** Sets the limit at which a sharp corner is drawn beveled. + Valid values are zero and greater. + Has no effect if miter is less than zero. + + @param miter zero and greater miter limit + + example: https://fiddle.skia.org/c/@Paint_setStrokeMiter + */ + void setStrokeMiter(SkScalar miter); + + /** \enum SkPaint::Cap + Cap draws at the beginning and end of an open path contour. + */ + enum Cap { + kButt_Cap, //!< no stroke extension + kRound_Cap, //!< adds circle + kSquare_Cap, //!< adds square + kLast_Cap = kSquare_Cap, //!< largest Cap value + kDefault_Cap = kButt_Cap, //!< equivalent to kButt_Cap + }; + + /** May be used to verify that SkPaint::Cap is a legal value. + */ + static constexpr int kCapCount = kLast_Cap + 1; + + /** \enum SkPaint::Join + Join specifies how corners are drawn when a shape is stroked. Join + affects the four corners of a stroked rectangle, and the connected segments in a + stroked path. + + Choose miter join to draw sharp corners. Choose round join to draw a circle with a + radius equal to the stroke width on top of the corner. Choose bevel join to minimally + connect the thick strokes. + + The fill path constructed to describe the stroked path respects the join setting but may + not contain the actual join. For instance, a fill path constructed with round joins does + not necessarily include circles at each connected segment. + */ + enum Join : uint8_t { + kMiter_Join, //!< extends to miter limit + kRound_Join, //!< adds circle + kBevel_Join, //!< connects outside edges + kLast_Join = kBevel_Join, //!< equivalent to the largest value for Join + kDefault_Join = kMiter_Join, //!< equivalent to kMiter_Join + }; + + /** May be used to verify that SkPaint::Join is a legal value. + */ + static constexpr int kJoinCount = kLast_Join + 1; + + /** Returns the geometry drawn at the beginning and end of strokes. + */ + Cap getStrokeCap() const { return (Cap)fBitfields.fCapType; } + + /** Sets the geometry drawn at the beginning and end of strokes. + + example: https://fiddle.skia.org/c/@Paint_setStrokeCap_a + example: https://fiddle.skia.org/c/@Paint_setStrokeCap_b + */ + void setStrokeCap(Cap cap); + + /** Returns the geometry drawn at the corners of strokes. + */ + Join getStrokeJoin() const { return (Join)fBitfields.fJoinType; } + + /** Sets the geometry drawn at the corners of strokes. + + example: https://fiddle.skia.org/c/@Paint_setStrokeJoin + */ + void setStrokeJoin(Join join); + + /** Returns optional colors used when filling a path, such as a gradient. + + Does not alter SkShader SkRefCnt. + + @return SkShader if previously set, nullptr otherwise + */ + SkShader* getShader() const { return fShader.get(); } + + /** Returns optional colors used when filling a path, such as a gradient. + + Increases SkShader SkRefCnt by one. + + @return SkShader if previously set, nullptr otherwise + + example: https://fiddle.skia.org/c/@Paint_refShader + */ + sk_sp refShader() const; + + /** Sets optional colors used when filling a path, such as a gradient. + + Sets SkShader to shader, decreasing SkRefCnt of the previous SkShader. + Increments shader SkRefCnt by one. + + @param shader how geometry is filled with color; if nullptr, color is used instead + + example: https://fiddle.skia.org/c/@Color_Filter_Methods + example: https://fiddle.skia.org/c/@Paint_setShader + */ + void setShader(sk_sp shader); + + /** Returns SkColorFilter if set, or nullptr. + Does not alter SkColorFilter SkRefCnt. + + @return SkColorFilter if previously set, nullptr otherwise + */ + SkColorFilter* getColorFilter() const { return fColorFilter.get(); } + + /** Returns SkColorFilter if set, or nullptr. + Increases SkColorFilter SkRefCnt by one. + + @return SkColorFilter if set, or nullptr + + example: https://fiddle.skia.org/c/@Paint_refColorFilter + */ + sk_sp refColorFilter() const; + + /** Sets SkColorFilter to filter, decreasing SkRefCnt of the previous + SkColorFilter. Pass nullptr to clear SkColorFilter. + + Increments filter SkRefCnt by one. + + @param colorFilter SkColorFilter to apply to subsequent draw + + example: https://fiddle.skia.org/c/@Blend_Mode_Methods + example: https://fiddle.skia.org/c/@Paint_setColorFilter + */ + void setColorFilter(sk_sp colorFilter); + + /** If the current blender can be represented as a SkBlendMode enum, this returns that + * enum in the optional's value(). If it cannot, then the returned optional does not + * contain a value. + */ + std::optional asBlendMode() const; + + /** + * Queries the blender, and if it can be represented as a SkBlendMode, return that mode, + * else return the defaultMode provided. + */ + SkBlendMode getBlendMode_or(SkBlendMode defaultMode) const; + + /** Returns true iff the current blender claims to be equivalent to SkBlendMode::kSrcOver. + * + * Also returns true of the current blender is nullptr. + */ + bool isSrcOver() const; + + /** Helper method for calling setBlender(). + * + * This sets a blender that implements the specified blendmode enum. + */ + void setBlendMode(SkBlendMode mode); + + /** Returns the user-supplied blend function, if one has been set. + * Does not alter SkBlender's SkRefCnt. + * + * A nullptr blender signifies the default SrcOver behavior. + * + * @return the SkBlender assigned to this paint, otherwise nullptr + */ + SkBlender* getBlender() const { return fBlender.get(); } + + /** Returns the user-supplied blend function, if one has been set. + * Increments the SkBlender's SkRefCnt by one. + * + * A nullptr blender signifies the default SrcOver behavior. + * + * @return the SkBlender assigned to this paint, otherwise nullptr + */ + sk_sp refBlender() const; + + /** Sets the current blender, increasing its refcnt, and if a blender is already + * present, decreasing that object's refcnt. + * + * A nullptr blender signifies the default SrcOver behavior. + * + * For convenience, you can call setBlendMode() if the blend effect can be expressed + * as one of those values. + */ + void setBlender(sk_sp blender); + + /** Returns SkPathEffect if set, or nullptr. + Does not alter SkPathEffect SkRefCnt. + + @return SkPathEffect if previously set, nullptr otherwise + */ + SkPathEffect* getPathEffect() const { return fPathEffect.get(); } + + /** Returns SkPathEffect if set, or nullptr. + Increases SkPathEffect SkRefCnt by one. + + @return SkPathEffect if previously set, nullptr otherwise + + example: https://fiddle.skia.org/c/@Paint_refPathEffect + */ + sk_sp refPathEffect() const; + + /** Sets SkPathEffect to pathEffect, decreasing SkRefCnt of the previous + SkPathEffect. Pass nullptr to leave the path geometry unaltered. + + Increments pathEffect SkRefCnt by one. + + @param pathEffect replace SkPath with a modification when drawn + + example: https://fiddle.skia.org/c/@Mask_Filter_Methods + example: https://fiddle.skia.org/c/@Paint_setPathEffect + */ + void setPathEffect(sk_sp pathEffect); + + /** Returns SkMaskFilter if set, or nullptr. + Does not alter SkMaskFilter SkRefCnt. + + @return SkMaskFilter if previously set, nullptr otherwise + */ + SkMaskFilter* getMaskFilter() const { return fMaskFilter.get(); } + + /** Returns SkMaskFilter if set, or nullptr. + + Increases SkMaskFilter SkRefCnt by one. + + @return SkMaskFilter if previously set, nullptr otherwise + + example: https://fiddle.skia.org/c/@Paint_refMaskFilter + */ + sk_sp refMaskFilter() const; + + /** Sets SkMaskFilter to maskFilter, decreasing SkRefCnt of the previous + SkMaskFilter. Pass nullptr to clear SkMaskFilter and leave SkMaskFilter effect on + mask alpha unaltered. + + Increments maskFilter SkRefCnt by one. + + @param maskFilter modifies clipping mask generated from drawn geometry + + example: https://fiddle.skia.org/c/@Paint_setMaskFilter + example: https://fiddle.skia.org/c/@Typeface_Methods + */ + void setMaskFilter(sk_sp maskFilter); + + /** Returns SkImageFilter if set, or nullptr. + Does not alter SkImageFilter SkRefCnt. + + @return SkImageFilter if previously set, nullptr otherwise + */ + SkImageFilter* getImageFilter() const { return fImageFilter.get(); } + + /** Returns SkImageFilter if set, or nullptr. + Increases SkImageFilter SkRefCnt by one. + + @return SkImageFilter if previously set, nullptr otherwise + + example: https://fiddle.skia.org/c/@Paint_refImageFilter + */ + sk_sp refImageFilter() const; + + /** Sets SkImageFilter to imageFilter, decreasing SkRefCnt of the previous + SkImageFilter. Pass nullptr to clear SkImageFilter, and remove SkImageFilter effect + on drawing. + + Increments imageFilter SkRefCnt by one. + + @param imageFilter how SkImage is sampled when transformed + + example: https://fiddle.skia.org/c/@Paint_setImageFilter + */ + void setImageFilter(sk_sp imageFilter); + + /** Returns true if SkPaint prevents all drawing; + otherwise, the SkPaint may or may not allow drawing. + + Returns true if, for example, SkBlendMode combined with alpha computes a + new alpha of zero. + + @return true if SkPaint prevents all drawing + + example: https://fiddle.skia.org/c/@Paint_nothingToDraw + */ + bool nothingToDraw() const; + + /** (to be made private) + Returns true if SkPaint does not include elements requiring extensive computation + to compute device bounds of drawn geometry. For instance, SkPaint with SkPathEffect + always returns false. + + @return true if SkPaint allows for fast computation of bounds + */ + bool canComputeFastBounds() const; + + /** (to be made private) + Only call this if canComputeFastBounds() returned true. This takes a + raw rectangle (the raw bounds of a shape), and adjusts it for stylistic + effects in the paint (e.g. stroking). If needed, it uses the storage + parameter. It returns the adjusted bounds that can then be used + for SkCanvas::quickReject tests. + + The returned SkRect will either be orig or storage, thus the caller + should not rely on storage being set to the result, but should always + use the returned value. It is legal for orig and storage to be the same + SkRect. + For example: + if (!path.isInverseFillType() && paint.canComputeFastBounds()) { + SkRect storage; + if (canvas->quickReject(paint.computeFastBounds(path.getBounds(), &storage))) { + return; // do not draw the path + } + } + // draw the path + + @param orig geometry modified by SkPaint when drawn + @param storage computed bounds of geometry; may not be nullptr + @return fast computed bounds + */ + const SkRect& computeFastBounds(const SkRect& orig, SkRect* storage) const; + + /** (to be made private) + + @param orig geometry modified by SkPaint when drawn + @param storage computed bounds of geometry + @return fast computed bounds + */ + const SkRect& computeFastStrokeBounds(const SkRect& orig, + SkRect* storage) const { + return this->doComputeFastBounds(orig, storage, kStroke_Style); + } + + /** (to be made private) + Computes the bounds, overriding the SkPaint SkPaint::Style. This can be used to + account for additional width required by stroking orig, without + altering SkPaint::Style set to fill. + + @param orig geometry modified by SkPaint when drawn + @param storage computed bounds of geometry + @param style overrides SkPaint::Style + @return fast computed bounds + */ + const SkRect& doComputeFastBounds(const SkRect& orig, SkRect* storage, + Style style) const; + + using sk_is_trivially_relocatable = std::true_type; + +private: + sk_sp fPathEffect; + sk_sp fShader; + sk_sp fMaskFilter; + sk_sp fColorFilter; + sk_sp fImageFilter; + sk_sp fBlender; + + SkColor4f fColor4f; + SkScalar fWidth; + SkScalar fMiterLimit; + union { + struct { + unsigned fAntiAlias : 1; + unsigned fDither : 1; + unsigned fCapType : 2; + unsigned fJoinType : 2; + unsigned fStyle : 2; + unsigned fPadding : 24; // 24 == 32 -1-1-2-2-2 + } fBitfields; + uint32_t fBitfieldsUInt; + }; + + static_assert(::sk_is_trivially_relocatable::value); + static_assert(::sk_is_trivially_relocatable::value); + static_assert(::sk_is_trivially_relocatable::value); + static_assert(::sk_is_trivially_relocatable::value); + static_assert(::sk_is_trivially_relocatable::value); + static_assert(::sk_is_trivially_relocatable::value); + static_assert(::sk_is_trivially_relocatable::value); + static_assert(::sk_is_trivially_relocatable::value); + + friend class SkPaintPriv; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPath.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPath.h new file mode 100644 index 00000000000..00324e989e1 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPath.h @@ -0,0 +1,1943 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPath_DEFINED +#define SkPath_DEFINED + +#include "include/core/SkMatrix.h" +#include "include/core/SkPathTypes.h" +#include "include/core/SkPoint.h" +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkDebug.h" +#include "include/private/base/SkTo.h" +#include "include/private/base/SkTypeTraits.h" + +#include +#include +#include +#include +#include +#include + +struct SkArc; +class SkData; +class SkPathRef; +class SkRRect; +class SkWStream; +enum class SkPathConvexity; +enum class SkPathFirstDirection; +struct SkPathVerbAnalysis; + +// WIP -- define this locally, and fix call-sites to use SkPathBuilder (skbug.com/9000) +//#define SK_HIDE_PATH_EDIT_METHODS + +/** \class SkPath + SkPath contain geometry. SkPath may be empty, or contain one or more verbs that + outline a figure. SkPath always starts with a move verb to a Cartesian coordinate, + and may be followed by additional verbs that add lines or curves. + Adding a close verb makes the geometry into a continuous loop, a closed contour. + SkPath may contain any number of contours, each beginning with a move verb. + + SkPath contours may contain only a move verb, or may also contain lines, + quadratic beziers, conics, and cubic beziers. SkPath contours may be open or + closed. + + When used to draw a filled area, SkPath describes whether the fill is inside or + outside the geometry. SkPath also describes the winding rule used to fill + overlapping contours. + + Internally, SkPath lazily computes metrics likes bounds and convexity. Call + SkPath::updateBoundsCache to make SkPath thread safe. +*/ +class SK_API SkPath { +public: + /** + * Create a new path with the specified segments. + * + * The points and weights arrays are read in order, based on the sequence of verbs. + * + * Move 1 point + * Line 1 point + * Quad 2 points + * Conic 2 points and 1 weight + * Cubic 3 points + * Close 0 points + * + * If an illegal sequence of verbs is encountered, or the specified number of points + * or weights is not sufficient given the verbs, an empty Path is returned. + * + * A legal sequence of verbs consists of any number of Contours. A contour always begins + * with a Move verb, followed by 0 or more segments: Line, Quad, Conic, Cubic, followed + * by an optional Close. + */ + static SkPath Make(const SkPoint[], int pointCount, + const uint8_t[], int verbCount, + const SkScalar[], int conicWeightCount, + SkPathFillType, bool isVolatile = false); + + static SkPath Rect(const SkRect&, SkPathDirection = SkPathDirection::kCW, + unsigned startIndex = 0); + static SkPath Oval(const SkRect&, SkPathDirection = SkPathDirection::kCW); + static SkPath Oval(const SkRect&, SkPathDirection, unsigned startIndex); + static SkPath Circle(SkScalar center_x, SkScalar center_y, SkScalar radius, + SkPathDirection dir = SkPathDirection::kCW); + static SkPath RRect(const SkRRect&, SkPathDirection dir = SkPathDirection::kCW); + static SkPath RRect(const SkRRect&, SkPathDirection, unsigned startIndex); + static SkPath RRect(const SkRect& bounds, SkScalar rx, SkScalar ry, + SkPathDirection dir = SkPathDirection::kCW); + + static SkPath Polygon(const SkPoint pts[], int count, bool isClosed, + SkPathFillType = SkPathFillType::kWinding, + bool isVolatile = false); + + static SkPath Polygon(const std::initializer_list& list, bool isClosed, + SkPathFillType fillType = SkPathFillType::kWinding, + bool isVolatile = false) { + return Polygon(list.begin(), SkToInt(list.size()), isClosed, fillType, isVolatile); + } + + static SkPath Line(const SkPoint a, const SkPoint b) { + return Polygon({a, b}, false); + } + + /** Constructs an empty SkPath. By default, SkPath has no verbs, no SkPoint, and no weights. + FillType is set to kWinding. + + @return empty SkPath + + example: https://fiddle.skia.org/c/@Path_empty_constructor + */ + SkPath(); + + /** Constructs a copy of an existing path. + Copy constructor makes two paths identical by value. Internally, path and + the returned result share pointer values. The underlying verb array, SkPoint array + and weights are copied when modified. + + Creating a SkPath copy is very efficient and never allocates memory. + SkPath are always copied by value from the interface; the underlying shared + pointers are not exposed. + + @param path SkPath to copy by value + @return copy of SkPath + + example: https://fiddle.skia.org/c/@Path_copy_const_SkPath + */ + SkPath(const SkPath& path); + + /** Releases ownership of any shared data and deletes data if SkPath is sole owner. + + example: https://fiddle.skia.org/c/@Path_destructor + */ + ~SkPath(); + + /** Returns a copy of this path in the current state. */ + SkPath snapshot() const { + return *this; + } + + /** Returns a copy of this path in the current state, and resets the path to empty. */ + SkPath detach() { + SkPath result = *this; + this->reset(); + return result; + } + + /** Constructs a copy of an existing path. + SkPath assignment makes two paths identical by value. Internally, assignment + shares pointer values. The underlying verb array, SkPoint array and weights + are copied when modified. + + Copying SkPath by assignment is very efficient and never allocates memory. + SkPath are always copied by value from the interface; the underlying shared + pointers are not exposed. + + @param path verb array, SkPoint array, weights, and SkPath::FillType to copy + @return SkPath copied by value + + example: https://fiddle.skia.org/c/@Path_copy_operator + */ + SkPath& operator=(const SkPath& path); + + /** Compares a and b; returns true if SkPath::FillType, verb array, SkPoint array, and weights + are equivalent. + + @param a SkPath to compare + @param b SkPath to compare + @return true if SkPath pair are equivalent + */ + friend SK_API bool operator==(const SkPath& a, const SkPath& b); + + /** Compares a and b; returns true if SkPath::FillType, verb array, SkPoint array, and weights + are not equivalent. + + @param a SkPath to compare + @param b SkPath to compare + @return true if SkPath pair are not equivalent + */ + friend bool operator!=(const SkPath& a, const SkPath& b) { + return !(a == b); + } + + /** Returns true if SkPath contain equal verbs and equal weights. + If SkPath contain one or more conics, the weights must match. + + conicTo() may add different verbs depending on conic weight, so it is not + trivial to interpolate a pair of SkPath containing conics with different + conic weight values. + + @param compare SkPath to compare + @return true if SkPath verb array and weights are equivalent + + example: https://fiddle.skia.org/c/@Path_isInterpolatable + */ + bool isInterpolatable(const SkPath& compare) const; + + /** Interpolates between SkPath with SkPoint array of equal size. + Copy verb array and weights to out, and set out SkPoint array to a weighted + average of this SkPoint array and ending SkPoint array, using the formula: + (Path Point * weight) + ending Point * (1 - weight). + + weight is most useful when between zero (ending SkPoint array) and + one (this Point_Array); will work with values outside of this + range. + + interpolate() returns false and leaves out unchanged if SkPoint array is not + the same size as ending SkPoint array. Call isInterpolatable() to check SkPath + compatibility prior to calling interpolate(). + + @param ending SkPoint array averaged with this SkPoint array + @param weight contribution of this SkPoint array, and + one minus contribution of ending SkPoint array + @param out SkPath replaced by interpolated averages + @return true if SkPath contain same number of SkPoint + + example: https://fiddle.skia.org/c/@Path_interpolate + */ + bool interpolate(const SkPath& ending, SkScalar weight, SkPath* out) const; + + /** Returns SkPathFillType, the rule used to fill SkPath. + + @return current SkPathFillType setting + */ + SkPathFillType getFillType() const { return (SkPathFillType)fFillType; } + + /** Sets FillType, the rule used to fill SkPath. While there is no check + that ft is legal, values outside of FillType are not supported. + */ + void setFillType(SkPathFillType ft) { + fFillType = SkToU8(ft); + } + + /** Returns if FillType describes area outside SkPath geometry. The inverse fill area + extends indefinitely. + + @return true if FillType is kInverseWinding or kInverseEvenOdd + */ + bool isInverseFillType() const { return SkPathFillType_IsInverse(this->getFillType()); } + + /** Replaces FillType with its inverse. The inverse of FillType describes the area + unmodified by the original FillType. + */ + void toggleInverseFillType() { + fFillType ^= 2; + } + + /** Returns true if the path is convex. If necessary, it will first compute the convexity. + */ + bool isConvex() const; + + /** Returns true if this path is recognized as an oval or circle. + + bounds receives bounds of oval. + + bounds is unmodified if oval is not found. + + @param bounds storage for bounding SkRect of oval; may be nullptr + @return true if SkPath is recognized as an oval or circle + + example: https://fiddle.skia.org/c/@Path_isOval + */ + bool isOval(SkRect* bounds) const; + + /** Returns true if path is representable as SkRRect. + Returns false if path is representable as oval, circle, or SkRect. + + rrect receives bounds of SkRRect. + + rrect is unmodified if SkRRect is not found. + + @param rrect storage for bounding SkRect of SkRRect; may be nullptr + @return true if SkPath contains only SkRRect + + example: https://fiddle.skia.org/c/@Path_isRRect + */ + bool isRRect(SkRRect* rrect) const; + + /** Returns true if path is representable as an oval arc. In other words, could this + path be drawn using SkCanvas::drawArc. + + arc receives parameters of arc + + @param arc storage for arc; may be nullptr + @return true if SkPath contains only a single arc from an oval + */ + bool isArc(SkArc* arc) const; + + /** Sets SkPath to its initial state. + Removes verb array, SkPoint array, and weights, and sets FillType to kWinding. + Internal storage associated with SkPath is released. + + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_reset + */ + SkPath& reset(); + + /** Sets SkPath to its initial state, preserving internal storage. + Removes verb array, SkPoint array, and weights, and sets FillType to kWinding. + Internal storage associated with SkPath is retained. + + Use rewind() instead of reset() if SkPath storage will be reused and performance + is critical. + + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_rewind + */ + SkPath& rewind(); + + /** Returns if SkPath is empty. + Empty SkPath may have FillType but has no SkPoint, SkPath::Verb, or conic weight. + SkPath() constructs empty SkPath; reset() and rewind() make SkPath empty. + + @return true if the path contains no SkPath::Verb array + */ + bool isEmpty() const; + + /** Returns if contour is closed. + Contour is closed if SkPath SkPath::Verb array was last modified by close(). When stroked, + closed contour draws SkPaint::Join instead of SkPaint::Cap at first and last SkPoint. + + @return true if the last contour ends with a kClose_Verb + + example: https://fiddle.skia.org/c/@Path_isLastContourClosed + */ + bool isLastContourClosed() const; + + /** Returns true for finite SkPoint array values between negative SK_ScalarMax and + positive SK_ScalarMax. Returns false for any SkPoint array value of + SK_ScalarInfinity, SK_ScalarNegativeInfinity, or SK_ScalarNaN. + + @return true if all SkPoint values are finite + */ + bool isFinite() const; + + /** Returns true if the path is volatile; it will not be altered or discarded + by the caller after it is drawn. SkPath by default have volatile set false, allowing + SkSurface to attach a cache of data which speeds repeated drawing. If true, SkSurface + may not speed repeated drawing. + + @return true if caller will alter SkPath after drawing + */ + bool isVolatile() const { + return SkToBool(fIsVolatile); + } + + /** Specifies whether SkPath is volatile; whether it will be altered or discarded + by the caller after it is drawn. SkPath by default have volatile set false, allowing + Skia to attach a cache of data which speeds repeated drawing. + + Mark temporary paths, discarded or modified after use, as volatile + to inform Skia that the path need not be cached. + + Mark animating SkPath volatile to improve performance. + Mark unchanging SkPath non-volatile to improve repeated rendering. + + raster surface SkPath draws are affected by volatile for some shadows. + GPU surface SkPath draws are affected by volatile for some shadows and concave geometries. + + @param isVolatile true if caller will alter SkPath after drawing + @return reference to SkPath + */ + SkPath& setIsVolatile(bool isVolatile) { + fIsVolatile = isVolatile; + return *this; + } + + /** Tests if line between SkPoint pair is degenerate. + Line with no length or that moves a very short distance is degenerate; it is + treated as a point. + + exact changes the equality test. If true, returns true only if p1 equals p2. + If false, returns true if p1 equals or nearly equals p2. + + @param p1 line start point + @param p2 line end point + @param exact if false, allow nearly equals + @return true if line is degenerate; its length is effectively zero + + example: https://fiddle.skia.org/c/@Path_IsLineDegenerate + */ + static bool IsLineDegenerate(const SkPoint& p1, const SkPoint& p2, bool exact); + + /** Tests if quad is degenerate. + Quad with no length or that moves a very short distance is degenerate; it is + treated as a point. + + @param p1 quad start point + @param p2 quad control point + @param p3 quad end point + @param exact if true, returns true only if p1, p2, and p3 are equal; + if false, returns true if p1, p2, and p3 are equal or nearly equal + @return true if quad is degenerate; its length is effectively zero + */ + static bool IsQuadDegenerate(const SkPoint& p1, const SkPoint& p2, + const SkPoint& p3, bool exact); + + /** Tests if cubic is degenerate. + Cubic with no length or that moves a very short distance is degenerate; it is + treated as a point. + + @param p1 cubic start point + @param p2 cubic control point 1 + @param p3 cubic control point 2 + @param p4 cubic end point + @param exact if true, returns true only if p1, p2, p3, and p4 are equal; + if false, returns true if p1, p2, p3, and p4 are equal or nearly equal + @return true if cubic is degenerate; its length is effectively zero + */ + static bool IsCubicDegenerate(const SkPoint& p1, const SkPoint& p2, + const SkPoint& p3, const SkPoint& p4, bool exact); + + /** Returns true if SkPath contains only one line; + SkPath::Verb array has two entries: kMove_Verb, kLine_Verb. + If SkPath contains one line and line is not nullptr, line is set to + line start point and line end point. + Returns false if SkPath is not one line; line is unaltered. + + @param line storage for line. May be nullptr + @return true if SkPath contains exactly one line + + example: https://fiddle.skia.org/c/@Path_isLine + */ + bool isLine(SkPoint line[2]) const; + + /** Returns the number of points in SkPath. + SkPoint count is initially zero. + + @return SkPath SkPoint array length + + example: https://fiddle.skia.org/c/@Path_countPoints + */ + int countPoints() const; + + /** Returns SkPoint at index in SkPoint array. Valid range for index is + 0 to countPoints() - 1. + Returns (0, 0) if index is out of range. + + @param index SkPoint array element selector + @return SkPoint array value or (0, 0) + + example: https://fiddle.skia.org/c/@Path_getPoint + */ + SkPoint getPoint(int index) const; + + /** Returns number of points in SkPath. Up to max points are copied. + points may be nullptr; then, max must be zero. + If max is greater than number of points, excess points storage is unaltered. + + @param points storage for SkPath SkPoint array. May be nullptr + @param max maximum to copy; must be greater than or equal to zero + @return SkPath SkPoint array length + + example: https://fiddle.skia.org/c/@Path_getPoints + */ + int getPoints(SkPoint points[], int max) const; + + /** Returns the number of verbs: kMove_Verb, kLine_Verb, kQuad_Verb, kConic_Verb, + kCubic_Verb, and kClose_Verb; added to SkPath. + + @return length of verb array + + example: https://fiddle.skia.org/c/@Path_countVerbs + */ + int countVerbs() const; + + /** Returns the number of verbs in the path. Up to max verbs are copied. The + verbs are copied as one byte per verb. + + @param verbs storage for verbs, may be nullptr + @param max maximum number to copy into verbs + @return the actual number of verbs in the path + + example: https://fiddle.skia.org/c/@Path_getVerbs + */ + int getVerbs(uint8_t verbs[], int max) const; + + /** Returns the approximate byte size of the SkPath in memory. + + @return approximate size + */ + size_t approximateBytesUsed() const; + + /** Exchanges the verb array, SkPoint array, weights, and SkPath::FillType with other. + Cached state is also exchanged. swap() internally exchanges pointers, so + it is lightweight and does not allocate memory. + + swap() usage has largely been replaced by operator=(const SkPath& path). + SkPath do not copy their content on assignment until they are written to, + making assignment as efficient as swap(). + + @param other SkPath exchanged by value + + example: https://fiddle.skia.org/c/@Path_swap + */ + void swap(SkPath& other); + + /** Returns minimum and maximum axes values of SkPoint array. + Returns (0, 0, 0, 0) if SkPath contains no points. Returned bounds width and height may + be larger or smaller than area affected when SkPath is drawn. + + SkRect returned includes all SkPoint added to SkPath, including SkPoint associated with + kMove_Verb that define empty contours. + + @return bounds of all SkPoint in SkPoint array + */ + const SkRect& getBounds() const; + + /** Updates internal bounds so that subsequent calls to getBounds() are instantaneous. + Unaltered copies of SkPath may also access cached bounds through getBounds(). + + For now, identical to calling getBounds() and ignoring the returned value. + + Call to prepare SkPath subsequently drawn from multiple threads, + to avoid a race condition where each draw separately computes the bounds. + */ + void updateBoundsCache() const { + // for now, just calling getBounds() is sufficient + this->getBounds(); + } + + /** Returns minimum and maximum axes values of the lines and curves in SkPath. + Returns (0, 0, 0, 0) if SkPath contains no points. + Returned bounds width and height may be larger or smaller than area affected + when SkPath is drawn. + + Includes SkPoint associated with kMove_Verb that define empty + contours. + + Behaves identically to getBounds() when SkPath contains + only lines. If SkPath contains curves, computed bounds includes + the maximum extent of the quad, conic, or cubic; is slower than getBounds(); + and unlike getBounds(), does not cache the result. + + @return tight bounds of curves in SkPath + + example: https://fiddle.skia.org/c/@Path_computeTightBounds + */ + SkRect computeTightBounds() const; + + /** Returns true if rect is contained by SkPath. + May return false when rect is contained by SkPath. + + For now, only returns true if SkPath has one contour and is convex. + rect may share points and edges with SkPath and be contained. + Returns true if rect is empty, that is, it has zero width or height; and + the SkPoint or line described by rect is contained by SkPath. + + @param rect SkRect, line, or SkPoint checked for containment + @return true if rect is contained + + example: https://fiddle.skia.org/c/@Path_conservativelyContainsRect + */ + bool conservativelyContainsRect(const SkRect& rect) const; + + /** Grows SkPath verb array, SkPoint array, and conics to contain additional space. + May improve performance and use less memory by + reducing the number and size of allocations when creating SkPath. + + @param extraPtCount number of additional SkPoint to allocate + @param extraVerbCount number of additional verbs + @param extraConicCount number of additional conics + + example: https://fiddle.skia.org/c/@Path_incReserve + */ + void incReserve(int extraPtCount, int extraVerbCount = 0, int extraConicCount = 0); + +#ifdef SK_HIDE_PATH_EDIT_METHODS +private: +#endif + + /** Adds beginning of contour at SkPoint (x, y). + + @param x x-axis value of contour start + @param y y-axis value of contour start + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_moveTo + */ + SkPath& moveTo(SkScalar x, SkScalar y); + + /** Adds beginning of contour at SkPoint p. + + @param p contour start + @return reference to SkPath + */ + SkPath& moveTo(const SkPoint& p) { + return this->moveTo(p.fX, p.fY); + } + + /** Adds beginning of contour relative to last point. + If SkPath is empty, starts contour at (dx, dy). + Otherwise, start contour at last point offset by (dx, dy). + Function name stands for "relative move to". + + @param dx offset from last point to contour start on x-axis + @param dy offset from last point to contour start on y-axis + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_rMoveTo + */ + SkPath& rMoveTo(SkScalar dx, SkScalar dy); + + /** Adds line from last point to (x, y). If SkPath is empty, or last SkPath::Verb is + kClose_Verb, last point is set to (0, 0) before adding line. + + lineTo() appends kMove_Verb to verb array and (0, 0) to SkPoint array, if needed. + lineTo() then appends kLine_Verb to verb array and (x, y) to SkPoint array. + + @param x end of added line on x-axis + @param y end of added line on y-axis + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_lineTo + */ + SkPath& lineTo(SkScalar x, SkScalar y); + + /** Adds line from last point to SkPoint p. If SkPath is empty, or last SkPath::Verb is + kClose_Verb, last point is set to (0, 0) before adding line. + + lineTo() first appends kMove_Verb to verb array and (0, 0) to SkPoint array, if needed. + lineTo() then appends kLine_Verb to verb array and SkPoint p to SkPoint array. + + @param p end SkPoint of added line + @return reference to SkPath + */ + SkPath& lineTo(const SkPoint& p) { + return this->lineTo(p.fX, p.fY); + } + + /** Adds line from last point to vector (dx, dy). If SkPath is empty, or last SkPath::Verb is + kClose_Verb, last point is set to (0, 0) before adding line. + + Appends kMove_Verb to verb array and (0, 0) to SkPoint array, if needed; + then appends kLine_Verb to verb array and line end to SkPoint array. + Line end is last point plus vector (dx, dy). + Function name stands for "relative line to". + + @param dx offset from last point to line end on x-axis + @param dy offset from last point to line end on y-axis + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_rLineTo + example: https://fiddle.skia.org/c/@Quad_a + example: https://fiddle.skia.org/c/@Quad_b + */ + SkPath& rLineTo(SkScalar dx, SkScalar dy); + + /** Adds quad from last point towards (x1, y1), to (x2, y2). + If SkPath is empty, or last SkPath::Verb is kClose_Verb, last point is set to (0, 0) + before adding quad. + + Appends kMove_Verb to verb array and (0, 0) to SkPoint array, if needed; + then appends kQuad_Verb to verb array; and (x1, y1), (x2, y2) + to SkPoint array. + + @param x1 control SkPoint of quad on x-axis + @param y1 control SkPoint of quad on y-axis + @param x2 end SkPoint of quad on x-axis + @param y2 end SkPoint of quad on y-axis + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_quadTo + */ + SkPath& quadTo(SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2); + + /** Adds quad from last point towards SkPoint p1, to SkPoint p2. + If SkPath is empty, or last SkPath::Verb is kClose_Verb, last point is set to (0, 0) + before adding quad. + + Appends kMove_Verb to verb array and (0, 0) to SkPoint array, if needed; + then appends kQuad_Verb to verb array; and SkPoint p1, p2 + to SkPoint array. + + @param p1 control SkPoint of added quad + @param p2 end SkPoint of added quad + @return reference to SkPath + */ + SkPath& quadTo(const SkPoint& p1, const SkPoint& p2) { + return this->quadTo(p1.fX, p1.fY, p2.fX, p2.fY); + } + + /** Adds quad from last point towards vector (dx1, dy1), to vector (dx2, dy2). + If SkPath is empty, or last SkPath::Verb + is kClose_Verb, last point is set to (0, 0) before adding quad. + + Appends kMove_Verb to verb array and (0, 0) to SkPoint array, + if needed; then appends kQuad_Verb to verb array; and appends quad + control and quad end to SkPoint array. + Quad control is last point plus vector (dx1, dy1). + Quad end is last point plus vector (dx2, dy2). + Function name stands for "relative quad to". + + @param dx1 offset from last point to quad control on x-axis + @param dy1 offset from last point to quad control on y-axis + @param dx2 offset from last point to quad end on x-axis + @param dy2 offset from last point to quad end on y-axis + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Conic_Weight_a + example: https://fiddle.skia.org/c/@Conic_Weight_b + example: https://fiddle.skia.org/c/@Conic_Weight_c + example: https://fiddle.skia.org/c/@Path_rQuadTo + */ + SkPath& rQuadTo(SkScalar dx1, SkScalar dy1, SkScalar dx2, SkScalar dy2); + + /** Adds conic from last point towards (x1, y1), to (x2, y2), weighted by w. + If SkPath is empty, or last SkPath::Verb is kClose_Verb, last point is set to (0, 0) + before adding conic. + + Appends kMove_Verb to verb array and (0, 0) to SkPoint array, if needed. + + If w is finite and not one, appends kConic_Verb to verb array; + and (x1, y1), (x2, y2) to SkPoint array; and w to conic weights. + + If w is one, appends kQuad_Verb to verb array, and + (x1, y1), (x2, y2) to SkPoint array. + + If w is not finite, appends kLine_Verb twice to verb array, and + (x1, y1), (x2, y2) to SkPoint array. + + @param x1 control SkPoint of conic on x-axis + @param y1 control SkPoint of conic on y-axis + @param x2 end SkPoint of conic on x-axis + @param y2 end SkPoint of conic on y-axis + @param w weight of added conic + @return reference to SkPath + */ + SkPath& conicTo(SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2, + SkScalar w); + + /** Adds conic from last point towards SkPoint p1, to SkPoint p2, weighted by w. + If SkPath is empty, or last SkPath::Verb is kClose_Verb, last point is set to (0, 0) + before adding conic. + + Appends kMove_Verb to verb array and (0, 0) to SkPoint array, if needed. + + If w is finite and not one, appends kConic_Verb to verb array; + and SkPoint p1, p2 to SkPoint array; and w to conic weights. + + If w is one, appends kQuad_Verb to verb array, and SkPoint p1, p2 + to SkPoint array. + + If w is not finite, appends kLine_Verb twice to verb array, and + SkPoint p1, p2 to SkPoint array. + + @param p1 control SkPoint of added conic + @param p2 end SkPoint of added conic + @param w weight of added conic + @return reference to SkPath + */ + SkPath& conicTo(const SkPoint& p1, const SkPoint& p2, SkScalar w) { + return this->conicTo(p1.fX, p1.fY, p2.fX, p2.fY, w); + } + + /** Adds conic from last point towards vector (dx1, dy1), to vector (dx2, dy2), + weighted by w. If SkPath is empty, or last SkPath::Verb + is kClose_Verb, last point is set to (0, 0) before adding conic. + + Appends kMove_Verb to verb array and (0, 0) to SkPoint array, + if needed. + + If w is finite and not one, next appends kConic_Verb to verb array, + and w is recorded as conic weight; otherwise, if w is one, appends + kQuad_Verb to verb array; or if w is not finite, appends kLine_Verb + twice to verb array. + + In all cases appends SkPoint control and end to SkPoint array. + control is last point plus vector (dx1, dy1). + end is last point plus vector (dx2, dy2). + + Function name stands for "relative conic to". + + @param dx1 offset from last point to conic control on x-axis + @param dy1 offset from last point to conic control on y-axis + @param dx2 offset from last point to conic end on x-axis + @param dy2 offset from last point to conic end on y-axis + @param w weight of added conic + @return reference to SkPath + */ + SkPath& rConicTo(SkScalar dx1, SkScalar dy1, SkScalar dx2, SkScalar dy2, + SkScalar w); + + /** Adds cubic from last point towards (x1, y1), then towards (x2, y2), ending at + (x3, y3). If SkPath is empty, or last SkPath::Verb is kClose_Verb, last point is set to + (0, 0) before adding cubic. + + Appends kMove_Verb to verb array and (0, 0) to SkPoint array, if needed; + then appends kCubic_Verb to verb array; and (x1, y1), (x2, y2), (x3, y3) + to SkPoint array. + + @param x1 first control SkPoint of cubic on x-axis + @param y1 first control SkPoint of cubic on y-axis + @param x2 second control SkPoint of cubic on x-axis + @param y2 second control SkPoint of cubic on y-axis + @param x3 end SkPoint of cubic on x-axis + @param y3 end SkPoint of cubic on y-axis + @return reference to SkPath + */ + SkPath& cubicTo(SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2, + SkScalar x3, SkScalar y3); + + /** Adds cubic from last point towards SkPoint p1, then towards SkPoint p2, ending at + SkPoint p3. If SkPath is empty, or last SkPath::Verb is kClose_Verb, last point is set to + (0, 0) before adding cubic. + + Appends kMove_Verb to verb array and (0, 0) to SkPoint array, if needed; + then appends kCubic_Verb to verb array; and SkPoint p1, p2, p3 + to SkPoint array. + + @param p1 first control SkPoint of cubic + @param p2 second control SkPoint of cubic + @param p3 end SkPoint of cubic + @return reference to SkPath + */ + SkPath& cubicTo(const SkPoint& p1, const SkPoint& p2, const SkPoint& p3) { + return this->cubicTo(p1.fX, p1.fY, p2.fX, p2.fY, p3.fX, p3.fY); + } + + /** Adds cubic from last point towards vector (dx1, dy1), then towards + vector (dx2, dy2), to vector (dx3, dy3). + If SkPath is empty, or last SkPath::Verb + is kClose_Verb, last point is set to (0, 0) before adding cubic. + + Appends kMove_Verb to verb array and (0, 0) to SkPoint array, + if needed; then appends kCubic_Verb to verb array; and appends cubic + control and cubic end to SkPoint array. + Cubic control is last point plus vector (dx1, dy1). + Cubic end is last point plus vector (dx2, dy2). + Function name stands for "relative cubic to". + + @param dx1 offset from last point to first cubic control on x-axis + @param dy1 offset from last point to first cubic control on y-axis + @param dx2 offset from last point to second cubic control on x-axis + @param dy2 offset from last point to second cubic control on y-axis + @param dx3 offset from last point to cubic end on x-axis + @param dy3 offset from last point to cubic end on y-axis + @return reference to SkPath + */ + SkPath& rCubicTo(SkScalar dx1, SkScalar dy1, SkScalar dx2, SkScalar dy2, + SkScalar dx3, SkScalar dy3); + + /** Appends arc to SkPath. Arc added is part of ellipse + bounded by oval, from startAngle through sweepAngle. Both startAngle and + sweepAngle are measured in degrees, where zero degrees is aligned with the + positive x-axis, and positive sweeps extends arc clockwise. + + arcTo() adds line connecting SkPath last SkPoint to initial arc SkPoint if forceMoveTo + is false and SkPath is not empty. Otherwise, added contour begins with first point + of arc. Angles greater than -360 and less than 360 are treated modulo 360. + + @param oval bounds of ellipse containing arc + @param startAngle starting angle of arc in degrees + @param sweepAngle sweep, in degrees. Positive is clockwise; treated modulo 360 + @param forceMoveTo true to start a new contour with arc + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_arcTo + */ + SkPath& arcTo(const SkRect& oval, SkScalar startAngle, SkScalar sweepAngle, bool forceMoveTo); + + /** Appends arc to SkPath, after appending line if needed. Arc is implemented by conic + weighted to describe part of circle. Arc is contained by tangent from + last SkPath point to (x1, y1), and tangent from (x1, y1) to (x2, y2). Arc + is part of circle sized to radius, positioned so it touches both tangent lines. + + If last Path Point does not start Arc, arcTo appends connecting Line to Path. + The length of Vector from (x1, y1) to (x2, y2) does not affect Arc. + + Arc sweep is always less than 180 degrees. If radius is zero, or if + tangents are nearly parallel, arcTo appends Line from last Path Point to (x1, y1). + + arcTo appends at most one Line and one conic. + arcTo implements the functionality of PostScript arct and HTML Canvas arcTo. + + @param x1 x-axis value common to pair of tangents + @param y1 y-axis value common to pair of tangents + @param x2 x-axis value end of second tangent + @param y2 y-axis value end of second tangent + @param radius distance from arc to circle center + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_arcTo_2_a + example: https://fiddle.skia.org/c/@Path_arcTo_2_b + example: https://fiddle.skia.org/c/@Path_arcTo_2_c + */ + SkPath& arcTo(SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2, SkScalar radius); + + /** Appends arc to SkPath, after appending line if needed. Arc is implemented by conic + weighted to describe part of circle. Arc is contained by tangent from + last SkPath point to p1, and tangent from p1 to p2. Arc + is part of circle sized to radius, positioned so it touches both tangent lines. + + If last SkPath SkPoint does not start arc, arcTo() appends connecting line to SkPath. + The length of vector from p1 to p2 does not affect arc. + + Arc sweep is always less than 180 degrees. If radius is zero, or if + tangents are nearly parallel, arcTo() appends line from last SkPath SkPoint to p1. + + arcTo() appends at most one line and one conic. + arcTo() implements the functionality of PostScript arct and HTML Canvas arcTo. + + @param p1 SkPoint common to pair of tangents + @param p2 end of second tangent + @param radius distance from arc to circle center + @return reference to SkPath + */ + SkPath& arcTo(const SkPoint p1, const SkPoint p2, SkScalar radius) { + return this->arcTo(p1.fX, p1.fY, p2.fX, p2.fY, radius); + } + + /** \enum SkPath::ArcSize + Four oval parts with radii (rx, ry) start at last SkPath SkPoint and ends at (x, y). + ArcSize and Direction select one of the four oval parts. + */ + enum ArcSize { + kSmall_ArcSize, //!< smaller of arc pair + kLarge_ArcSize, //!< larger of arc pair + }; + + /** Appends arc to SkPath. Arc is implemented by one or more conics weighted to + describe part of oval with radii (rx, ry) rotated by xAxisRotate degrees. Arc + curves from last SkPath SkPoint to (x, y), choosing one of four possible routes: + clockwise or counterclockwise, and smaller or larger. + + Arc sweep is always less than 360 degrees. arcTo() appends line to (x, y) if + either radii are zero, or if last SkPath SkPoint equals (x, y). arcTo() scales radii + (rx, ry) to fit last SkPath SkPoint and (x, y) if both are greater than zero but + too small. + + arcTo() appends up to four conic curves. + arcTo() implements the functionality of SVG arc, although SVG sweep-flag value + is opposite the integer value of sweep; SVG sweep-flag uses 1 for clockwise, + while kCW_Direction cast to int is zero. + + @param rx radius on x-axis before x-axis rotation + @param ry radius on y-axis before x-axis rotation + @param xAxisRotate x-axis rotation in degrees; positive values are clockwise + @param largeArc chooses smaller or larger arc + @param sweep chooses clockwise or counterclockwise arc + @param x end of arc + @param y end of arc + @return reference to SkPath + */ + SkPath& arcTo(SkScalar rx, SkScalar ry, SkScalar xAxisRotate, ArcSize largeArc, + SkPathDirection sweep, SkScalar x, SkScalar y); + + /** Appends arc to SkPath. Arc is implemented by one or more conic weighted to describe + part of oval with radii (r.fX, r.fY) rotated by xAxisRotate degrees. Arc curves + from last SkPath SkPoint to (xy.fX, xy.fY), choosing one of four possible routes: + clockwise or counterclockwise, + and smaller or larger. + + Arc sweep is always less than 360 degrees. arcTo() appends line to xy if either + radii are zero, or if last SkPath SkPoint equals (xy.fX, xy.fY). arcTo() scales radii r to + fit last SkPath SkPoint and xy if both are greater than zero but too small to describe + an arc. + + arcTo() appends up to four conic curves. + arcTo() implements the functionality of SVG arc, although SVG sweep-flag value is + opposite the integer value of sweep; SVG sweep-flag uses 1 for clockwise, while + kCW_Direction cast to int is zero. + + @param r radii on axes before x-axis rotation + @param xAxisRotate x-axis rotation in degrees; positive values are clockwise + @param largeArc chooses smaller or larger arc + @param sweep chooses clockwise or counterclockwise arc + @param xy end of arc + @return reference to SkPath + */ + SkPath& arcTo(const SkPoint r, SkScalar xAxisRotate, ArcSize largeArc, SkPathDirection sweep, + const SkPoint xy) { + return this->arcTo(r.fX, r.fY, xAxisRotate, largeArc, sweep, xy.fX, xy.fY); + } + + /** Appends arc to SkPath, relative to last SkPath SkPoint. Arc is implemented by one or + more conic, weighted to describe part of oval with radii (rx, ry) rotated by + xAxisRotate degrees. Arc curves from last SkPath SkPoint to relative end SkPoint: + (dx, dy), choosing one of four possible routes: clockwise or + counterclockwise, and smaller or larger. If SkPath is empty, the start arc SkPoint + is (0, 0). + + Arc sweep is always less than 360 degrees. arcTo() appends line to end SkPoint + if either radii are zero, or if last SkPath SkPoint equals end SkPoint. + arcTo() scales radii (rx, ry) to fit last SkPath SkPoint and end SkPoint if both are + greater than zero but too small to describe an arc. + + arcTo() appends up to four conic curves. + arcTo() implements the functionality of svg arc, although SVG "sweep-flag" value is + opposite the integer value of sweep; SVG "sweep-flag" uses 1 for clockwise, while + kCW_Direction cast to int is zero. + + @param rx radius before x-axis rotation + @param ry radius before x-axis rotation + @param xAxisRotate x-axis rotation in degrees; positive values are clockwise + @param largeArc chooses smaller or larger arc + @param sweep chooses clockwise or counterclockwise arc + @param dx x-axis offset end of arc from last SkPath SkPoint + @param dy y-axis offset end of arc from last SkPath SkPoint + @return reference to SkPath + */ + SkPath& rArcTo(SkScalar rx, SkScalar ry, SkScalar xAxisRotate, ArcSize largeArc, + SkPathDirection sweep, SkScalar dx, SkScalar dy); + + /** Appends kClose_Verb to SkPath. A closed contour connects the first and last SkPoint + with line, forming a continuous loop. Open and closed contour draw the same + with SkPaint::kFill_Style. With SkPaint::kStroke_Style, open contour draws + SkPaint::Cap at contour start and end; closed contour draws + SkPaint::Join at contour start and end. + + close() has no effect if SkPath is empty or last SkPath SkPath::Verb is kClose_Verb. + + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_close + */ + SkPath& close(); + +#ifdef SK_HIDE_PATH_EDIT_METHODS +public: +#endif + + /** Approximates conic with quad array. Conic is constructed from start SkPoint p0, + control SkPoint p1, end SkPoint p2, and weight w. + Quad array is stored in pts; this storage is supplied by caller. + Maximum quad count is 2 to the pow2. + Every third point in array shares last SkPoint of previous quad and first SkPoint of + next quad. Maximum pts storage size is given by: + (1 + 2 * (1 << pow2)) * sizeof(SkPoint). + + Returns quad count used the approximation, which may be smaller + than the number requested. + + conic weight determines the amount of influence conic control point has on the curve. + w less than one represents an elliptical section. w greater than one represents + a hyperbolic section. w equal to one represents a parabolic section. + + Two quad curves are sufficient to approximate an elliptical conic with a sweep + of up to 90 degrees; in this case, set pow2 to one. + + @param p0 conic start SkPoint + @param p1 conic control SkPoint + @param p2 conic end SkPoint + @param w conic weight + @param pts storage for quad array + @param pow2 quad count, as power of two, normally 0 to 5 (1 to 32 quad curves) + @return number of quad curves written to pts + */ + static int ConvertConicToQuads(const SkPoint& p0, const SkPoint& p1, const SkPoint& p2, + SkScalar w, SkPoint pts[], int pow2); + + /** Returns true if SkPath is equivalent to SkRect when filled. + If false: rect, isClosed, and direction are unchanged. + If true: rect, isClosed, and direction are written to if not nullptr. + + rect may be smaller than the SkPath bounds. SkPath bounds may include kMove_Verb points + that do not alter the area drawn by the returned rect. + + @param rect storage for bounds of SkRect; may be nullptr + @param isClosed storage set to true if SkPath is closed; may be nullptr + @param direction storage set to SkRect direction; may be nullptr + @return true if SkPath contains SkRect + + example: https://fiddle.skia.org/c/@Path_isRect + */ + bool isRect(SkRect* rect, bool* isClosed = nullptr, SkPathDirection* direction = nullptr) const; + +#ifdef SK_HIDE_PATH_EDIT_METHODS +private: +#endif + + /** Adds a new contour to the path, defined by the rect, and wound in the + specified direction. The verbs added to the path will be: + + kMove, kLine, kLine, kLine, kClose + + start specifies which corner to begin the contour: + 0: upper-left corner + 1: upper-right corner + 2: lower-right corner + 3: lower-left corner + + This start point also acts as the implied beginning of the subsequent, + contour, if it does not have an explicit moveTo(). e.g. + + path.addRect(...) + // if we don't say moveTo() here, we will use the rect's start point + path.lineTo(...) + + @param rect SkRect to add as a closed contour + @param dir SkPath::Direction to orient the new contour + @param start initial corner of SkRect to add + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_addRect_2 + */ + SkPath& addRect(const SkRect& rect, SkPathDirection dir, unsigned start); + + SkPath& addRect(const SkRect& rect, SkPathDirection dir = SkPathDirection::kCW) { + return this->addRect(rect, dir, 0); + } + + SkPath& addRect(SkScalar left, SkScalar top, SkScalar right, SkScalar bottom, + SkPathDirection dir = SkPathDirection::kCW) { + return this->addRect({left, top, right, bottom}, dir, 0); + } + + /** Adds oval to path, appending kMove_Verb, four kConic_Verb, and kClose_Verb. + Oval is upright ellipse bounded by SkRect oval with radii equal to half oval width + and half oval height. Oval begins at (oval.fRight, oval.centerY()) and continues + clockwise if dir is kCW_Direction, counterclockwise if dir is kCCW_Direction. + + @param oval bounds of ellipse added + @param dir SkPath::Direction to wind ellipse + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_addOval + */ + SkPath& addOval(const SkRect& oval, SkPathDirection dir = SkPathDirection::kCW); + + /** Adds oval to SkPath, appending kMove_Verb, four kConic_Verb, and kClose_Verb. + Oval is upright ellipse bounded by SkRect oval with radii equal to half oval width + and half oval height. Oval begins at start and continues + clockwise if dir is kCW_Direction, counterclockwise if dir is kCCW_Direction. + + @param oval bounds of ellipse added + @param dir SkPath::Direction to wind ellipse + @param start index of initial point of ellipse + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_addOval_2 + */ + SkPath& addOval(const SkRect& oval, SkPathDirection dir, unsigned start); + + /** Experimental, subject to change or removal. + + Adds an "open" oval to SkPath. This follows canvas2D semantics: The oval is not + a separate contour. If the path was empty, then kMove_Verb is appended. Otherwise, + kLine_Verb is appended. Four kConic_Verbs are appended. kClose_Verb is not appended. + */ + SkPath& addOpenOval(const SkRect& oval, SkPathDirection dir, unsigned start); + + /** Adds circle centered at (x, y) of size radius to SkPath, appending kMove_Verb, + four kConic_Verb, and kClose_Verb. Circle begins at: (x + radius, y), continuing + clockwise if dir is kCW_Direction, and counterclockwise if dir is kCCW_Direction. + + Has no effect if radius is zero or negative. + + @param x center of circle + @param y center of circle + @param radius distance from center to edge + @param dir SkPath::Direction to wind circle + @return reference to SkPath + */ + SkPath& addCircle(SkScalar x, SkScalar y, SkScalar radius, + SkPathDirection dir = SkPathDirection::kCW); + + /** Appends arc to SkPath, as the start of new contour. Arc added is part of ellipse + bounded by oval, from startAngle through sweepAngle. Both startAngle and + sweepAngle are measured in degrees, where zero degrees is aligned with the + positive x-axis, and positive sweeps extends arc clockwise. + + If sweepAngle <= -360, or sweepAngle >= 360; and startAngle modulo 90 is nearly + zero, append oval instead of arc. Otherwise, sweepAngle values are treated + modulo 360, and arc may or may not draw depending on numeric rounding. + + @param oval bounds of ellipse containing arc + @param startAngle starting angle of arc in degrees + @param sweepAngle sweep, in degrees. Positive is clockwise; treated modulo 360 + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_addArc + */ + SkPath& addArc(const SkRect& oval, SkScalar startAngle, SkScalar sweepAngle); + + /** Appends SkRRect to SkPath, creating a new closed contour. SkRRect has bounds + equal to rect; each corner is 90 degrees of an ellipse with radii (rx, ry). If + dir is kCW_Direction, SkRRect starts at top-left of the lower-left corner and + winds clockwise. If dir is kCCW_Direction, SkRRect starts at the bottom-left + of the upper-left corner and winds counterclockwise. + + If either rx or ry is too large, rx and ry are scaled uniformly until the + corners fit. If rx or ry is less than or equal to zero, addRoundRect() appends + SkRect rect to SkPath. + + After appending, SkPath may be empty, or may contain: SkRect, oval, or SkRRect. + + @param rect bounds of SkRRect + @param rx x-axis radius of rounded corners on the SkRRect + @param ry y-axis radius of rounded corners on the SkRRect + @param dir SkPath::Direction to wind SkRRect + @return reference to SkPath + */ + SkPath& addRoundRect(const SkRect& rect, SkScalar rx, SkScalar ry, + SkPathDirection dir = SkPathDirection::kCW); + + /** Appends SkRRect to SkPath, creating a new closed contour. SkRRect has bounds + equal to rect; each corner is 90 degrees of an ellipse with radii from the + array. + + @param rect bounds of SkRRect + @param radii array of 8 SkScalar values, a radius pair for each corner + @param dir SkPath::Direction to wind SkRRect + @return reference to SkPath + */ + SkPath& addRoundRect(const SkRect& rect, const SkScalar radii[], + SkPathDirection dir = SkPathDirection::kCW); + + /** Adds rrect to SkPath, creating a new closed contour. If + dir is kCW_Direction, rrect starts at top-left of the lower-left corner and + winds clockwise. If dir is kCCW_Direction, rrect starts at the bottom-left + of the upper-left corner and winds counterclockwise. + + After appending, SkPath may be empty, or may contain: SkRect, oval, or SkRRect. + + @param rrect bounds and radii of rounded rectangle + @param dir SkPath::Direction to wind SkRRect + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_addRRect + */ + SkPath& addRRect(const SkRRect& rrect, SkPathDirection dir = SkPathDirection::kCW); + + /** Adds rrect to SkPath, creating a new closed contour. If dir is kCW_Direction, rrect + winds clockwise; if dir is kCCW_Direction, rrect winds counterclockwise. + start determines the first point of rrect to add. + + @param rrect bounds and radii of rounded rectangle + @param dir SkPath::Direction to wind SkRRect + @param start index of initial point of SkRRect + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_addRRect_2 + */ + SkPath& addRRect(const SkRRect& rrect, SkPathDirection dir, unsigned start); + + /** Adds contour created from line array, adding (count - 1) line segments. + Contour added starts at pts[0], then adds a line for every additional SkPoint + in pts array. If close is true, appends kClose_Verb to SkPath, connecting + pts[count - 1] and pts[0]. + + If count is zero, append kMove_Verb to path. + Has no effect if count is less than one. + + @param pts array of line sharing end and start SkPoint + @param count length of SkPoint array + @param close true to add line connecting contour end and start + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_addPoly + */ + SkPath& addPoly(const SkPoint pts[], int count, bool close); + + /** Adds contour created from list. Contour added starts at list[0], then adds a line + for every additional SkPoint in list. If close is true, appends kClose_Verb to SkPath, + connecting last and first SkPoint in list. + + If list is empty, append kMove_Verb to path. + + @param list array of SkPoint + @param close true to add line connecting contour end and start + @return reference to SkPath + */ + SkPath& addPoly(const std::initializer_list& list, bool close) { + return this->addPoly(list.begin(), SkToInt(list.size()), close); + } + +#ifdef SK_HIDE_PATH_EDIT_METHODS +public: +#endif + + /** \enum SkPath::AddPathMode + AddPathMode chooses how addPath() appends. Adding one SkPath to another can extend + the last contour or start a new contour. + */ + enum AddPathMode { + /** Contours are appended to the destination path as new contours. + */ + kAppend_AddPathMode, + /** Extends the last contour of the destination path with the first countour + of the source path, connecting them with a line. If the last contour is + closed, a new empty contour starting at its start point is extended instead. + If the destination path is empty, the result is the source path. + The last path of the result is closed only if the last path of the source is. + */ + kExtend_AddPathMode, + }; + + /** Appends src to SkPath, offset by (dx, dy). + + If mode is kAppend_AddPathMode, src verb array, SkPoint array, and conic weights are + added unaltered. If mode is kExtend_AddPathMode, add line before appending + verbs, SkPoint, and conic weights. + + @param src SkPath verbs, SkPoint, and conic weights to add + @param dx offset added to src SkPoint array x-axis coordinates + @param dy offset added to src SkPoint array y-axis coordinates + @param mode kAppend_AddPathMode or kExtend_AddPathMode + @return reference to SkPath + */ + SkPath& addPath(const SkPath& src, SkScalar dx, SkScalar dy, + AddPathMode mode = kAppend_AddPathMode); + + /** Appends src to SkPath. + + If mode is kAppend_AddPathMode, src verb array, SkPoint array, and conic weights are + added unaltered. If mode is kExtend_AddPathMode, add line before appending + verbs, SkPoint, and conic weights. + + @param src SkPath verbs, SkPoint, and conic weights to add + @param mode kAppend_AddPathMode or kExtend_AddPathMode + @return reference to SkPath + */ + SkPath& addPath(const SkPath& src, AddPathMode mode = kAppend_AddPathMode) { + SkMatrix m; + m.reset(); + return this->addPath(src, m, mode); + } + + /** Appends src to SkPath, transformed by matrix. Transformed curves may have different + verbs, SkPoint, and conic weights. + + If mode is kAppend_AddPathMode, src verb array, SkPoint array, and conic weights are + added unaltered. If mode is kExtend_AddPathMode, add line before appending + verbs, SkPoint, and conic weights. + + @param src SkPath verbs, SkPoint, and conic weights to add + @param matrix transform applied to src + @param mode kAppend_AddPathMode or kExtend_AddPathMode + @return reference to SkPath + */ + SkPath& addPath(const SkPath& src, const SkMatrix& matrix, + AddPathMode mode = kAppend_AddPathMode); + + /** Appends src to SkPath, from back to front. + Reversed src always appends a new contour to SkPath. + + @param src SkPath verbs, SkPoint, and conic weights to add + @return reference to SkPath + + example: https://fiddle.skia.org/c/@Path_reverseAddPath + */ + SkPath& reverseAddPath(const SkPath& src); + + /** Offsets SkPoint array by (dx, dy). Offset SkPath replaces dst. + If dst is nullptr, SkPath is replaced by offset data. + + @param dx offset added to SkPoint array x-axis coordinates + @param dy offset added to SkPoint array y-axis coordinates + @param dst overwritten, translated copy of SkPath; may be nullptr + + example: https://fiddle.skia.org/c/@Path_offset + */ + void offset(SkScalar dx, SkScalar dy, SkPath* dst) const; + + /** Offsets SkPoint array by (dx, dy). SkPath is replaced by offset data. + + @param dx offset added to SkPoint array x-axis coordinates + @param dy offset added to SkPoint array y-axis coordinates + */ + SkPath& offset(SkScalar dx, SkScalar dy) { + this->offset(dx, dy, this); + return *this; + } + + /** Transforms verb array, SkPoint array, and weight by matrix. + transform may change verbs and increase their number. + Transformed SkPath replaces dst; if dst is nullptr, original data + is replaced. + + @param matrix SkMatrix to apply to SkPath + @param dst overwritten, transformed copy of SkPath; may be nullptr + @param pc whether to apply perspective clipping + + example: https://fiddle.skia.org/c/@Path_transform + */ + void transform(const SkMatrix& matrix, SkPath* dst, + SkApplyPerspectiveClip pc = SkApplyPerspectiveClip::kYes) const; + + /** Transforms verb array, SkPoint array, and weight by matrix. + transform may change verbs and increase their number. + SkPath is replaced by transformed data. + + @param matrix SkMatrix to apply to SkPath + @param pc whether to apply perspective clipping + */ + SkPath& transform(const SkMatrix& matrix, + SkApplyPerspectiveClip pc = SkApplyPerspectiveClip::kYes) { + this->transform(matrix, this, pc); + return *this; + } + + SkPath makeTransform(const SkMatrix& m, + SkApplyPerspectiveClip pc = SkApplyPerspectiveClip::kYes) const { + SkPath dst; + this->transform(m, &dst, pc); + return dst; + } + + SkPath makeScale(SkScalar sx, SkScalar sy) { + return this->makeTransform(SkMatrix::Scale(sx, sy), SkApplyPerspectiveClip::kNo); + } + + /** Returns last point on SkPath in lastPt. Returns false if SkPoint array is empty, + storing (0, 0) if lastPt is not nullptr. + + @param lastPt storage for final SkPoint in SkPoint array; may be nullptr + @return true if SkPoint array contains one or more SkPoint + + example: https://fiddle.skia.org/c/@Path_getLastPt + */ + bool getLastPt(SkPoint* lastPt) const; + + /** Sets last point to (x, y). If SkPoint array is empty, append kMove_Verb to + verb array and append (x, y) to SkPoint array. + + @param x set x-axis value of last point + @param y set y-axis value of last point + + example: https://fiddle.skia.org/c/@Path_setLastPt + */ + void setLastPt(SkScalar x, SkScalar y); + + /** Sets the last point on the path. If SkPoint array is empty, append kMove_Verb to + verb array and append p to SkPoint array. + + @param p set value of last point + */ + void setLastPt(const SkPoint& p) { + this->setLastPt(p.fX, p.fY); + } + + /** \enum SkPath::SegmentMask + SegmentMask constants correspond to each drawing Verb type in SkPath; for + instance, if SkPath only contains lines, only the kLine_SegmentMask bit is set. + */ + enum SegmentMask { + kLine_SegmentMask = kLine_SkPathSegmentMask, + kQuad_SegmentMask = kQuad_SkPathSegmentMask, + kConic_SegmentMask = kConic_SkPathSegmentMask, + kCubic_SegmentMask = kCubic_SkPathSegmentMask, + }; + + /** Returns a mask, where each set bit corresponds to a SegmentMask constant + if SkPath contains one or more verbs of that type. + Returns zero if SkPath contains no lines, or curves: quads, conics, or cubics. + + getSegmentMasks() returns a cached result; it is very fast. + + @return SegmentMask bits or zero + */ + uint32_t getSegmentMasks() const; + + /** \enum SkPath::Verb + Verb instructs SkPath how to interpret one or more SkPoint and optional conic weight; + manage contour, and terminate SkPath. + */ + enum Verb { + kMove_Verb = static_cast(SkPathVerb::kMove), + kLine_Verb = static_cast(SkPathVerb::kLine), + kQuad_Verb = static_cast(SkPathVerb::kQuad), + kConic_Verb = static_cast(SkPathVerb::kConic), + kCubic_Verb = static_cast(SkPathVerb::kCubic), + kClose_Verb = static_cast(SkPathVerb::kClose), + kDone_Verb = kClose_Verb + 1 + }; + + /** \class SkPath::Iter + Iterates through verb array, and associated SkPoint array and conic weight. + Provides options to treat open contours as closed, and to ignore + degenerate data. + */ + class SK_API Iter { + public: + + /** Initializes SkPath::Iter with an empty SkPath. next() on SkPath::Iter returns + kDone_Verb. + Call setPath to initialize SkPath::Iter at a later time. + + @return SkPath::Iter of empty SkPath + + example: https://fiddle.skia.org/c/@Path_Iter_Iter + */ + Iter(); + + /** Sets SkPath::Iter to return elements of verb array, SkPoint array, and conic weight in + path. If forceClose is true, SkPath::Iter will add kLine_Verb and kClose_Verb after each + open contour. path is not altered. + + @param path SkPath to iterate + @param forceClose true if open contours generate kClose_Verb + @return SkPath::Iter of path + + example: https://fiddle.skia.org/c/@Path_Iter_const_SkPath + */ + Iter(const SkPath& path, bool forceClose); + + /** Sets SkPath::Iter to return elements of verb array, SkPoint array, and conic weight in + path. If forceClose is true, SkPath::Iter will add kLine_Verb and kClose_Verb after each + open contour. path is not altered. + + @param path SkPath to iterate + @param forceClose true if open contours generate kClose_Verb + + example: https://fiddle.skia.org/c/@Path_Iter_setPath + */ + void setPath(const SkPath& path, bool forceClose); + + /** Returns next SkPath::Verb in verb array, and advances SkPath::Iter. + When verb array is exhausted, returns kDone_Verb. + + Zero to four SkPoint are stored in pts, depending on the returned SkPath::Verb. + + @param pts storage for SkPoint data describing returned SkPath::Verb + @return next SkPath::Verb from verb array + + example: https://fiddle.skia.org/c/@Path_RawIter_next + */ + Verb next(SkPoint pts[4]); + + /** Returns conic weight if next() returned kConic_Verb. + + If next() has not been called, or next() did not return kConic_Verb, + result is undefined. + + @return conic weight for conic SkPoint returned by next() + */ + SkScalar conicWeight() const { return *fConicWeights; } + + /** Returns true if last kLine_Verb returned by next() was generated + by kClose_Verb. When true, the end point returned by next() is + also the start point of contour. + + If next() has not been called, or next() did not return kLine_Verb, + result is undefined. + + @return true if last kLine_Verb was generated by kClose_Verb + */ + bool isCloseLine() const { return SkToBool(fCloseLine); } + + /** Returns true if subsequent calls to next() return kClose_Verb before returning + kMove_Verb. if true, contour SkPath::Iter is processing may end with kClose_Verb, or + SkPath::Iter may have been initialized with force close set to true. + + @return true if contour is closed + + example: https://fiddle.skia.org/c/@Path_Iter_isClosedContour + */ + bool isClosedContour() const; + + private: + const SkPoint* fPts; + const uint8_t* fVerbs; + const uint8_t* fVerbStop; + const SkScalar* fConicWeights; + SkPoint fMoveTo; + SkPoint fLastPt; + bool fForceClose; + bool fNeedClose; + bool fCloseLine; + + Verb autoClose(SkPoint pts[2]); + }; + +private: + /** \class SkPath::RangeIter + Iterates through a raw range of path verbs, points, and conics. All values are returned + unaltered. + + NOTE: This class will be moved into SkPathPriv once RangeIter is removed. + */ + class RangeIter { + public: + RangeIter() = default; + RangeIter(const uint8_t* verbs, const SkPoint* points, const SkScalar* weights) + : fVerb(verbs), fPoints(points), fWeights(weights) { + SkDEBUGCODE(fInitialPoints = fPoints;) + } + bool operator!=(const RangeIter& that) const { + return fVerb != that.fVerb; + } + bool operator==(const RangeIter& that) const { + return fVerb == that.fVerb; + } + RangeIter& operator++() { + auto verb = static_cast(*fVerb++); + fPoints += pts_advance_after_verb(verb); + if (verb == SkPathVerb::kConic) { + ++fWeights; + } + return *this; + } + RangeIter operator++(int) { + RangeIter copy = *this; + this->operator++(); + return copy; + } + SkPathVerb peekVerb() const { + return static_cast(*fVerb); + } + std::tuple operator*() const { + SkPathVerb verb = this->peekVerb(); + // We provide the starting point for beziers by peeking backwards from the current + // point, which works fine as long as there is always a kMove before any geometry. + // (SkPath::injectMoveToIfNeeded should have guaranteed this to be the case.) + int backset = pts_backset_for_verb(verb); + SkASSERT(fPoints + backset >= fInitialPoints); + return {verb, fPoints + backset, fWeights}; + } + private: + constexpr static int pts_advance_after_verb(SkPathVerb verb) { + switch (verb) { + case SkPathVerb::kMove: return 1; + case SkPathVerb::kLine: return 1; + case SkPathVerb::kQuad: return 2; + case SkPathVerb::kConic: return 2; + case SkPathVerb::kCubic: return 3; + case SkPathVerb::kClose: return 0; + } + SkUNREACHABLE; + } + constexpr static int pts_backset_for_verb(SkPathVerb verb) { + switch (verb) { + case SkPathVerb::kMove: return 0; + case SkPathVerb::kLine: return -1; + case SkPathVerb::kQuad: return -1; + case SkPathVerb::kConic: return -1; + case SkPathVerb::kCubic: return -1; + case SkPathVerb::kClose: return -1; + } + SkUNREACHABLE; + } + const uint8_t* fVerb = nullptr; + const SkPoint* fPoints = nullptr; + const SkScalar* fWeights = nullptr; + SkDEBUGCODE(const SkPoint* fInitialPoints = nullptr;) + }; +public: + + /** \class SkPath::RawIter + Use Iter instead. This class will soon be removed and RangeIter will be made private. + */ + class SK_API RawIter { + public: + + /** Initializes RawIter with an empty SkPath. next() on RawIter returns kDone_Verb. + Call setPath to initialize SkPath::Iter at a later time. + + @return RawIter of empty SkPath + */ + RawIter() {} + + /** Sets RawIter to return elements of verb array, SkPoint array, and conic weight in path. + + @param path SkPath to iterate + @return RawIter of path + */ + RawIter(const SkPath& path) { + setPath(path); + } + + /** Sets SkPath::Iter to return elements of verb array, SkPoint array, and conic weight in + path. + + @param path SkPath to iterate + */ + void setPath(const SkPath&); + + /** Returns next SkPath::Verb in verb array, and advances RawIter. + When verb array is exhausted, returns kDone_Verb. + Zero to four SkPoint are stored in pts, depending on the returned SkPath::Verb. + + @param pts storage for SkPoint data describing returned SkPath::Verb + @return next SkPath::Verb from verb array + */ + Verb next(SkPoint[4]); + + /** Returns next SkPath::Verb, but does not advance RawIter. + + @return next SkPath::Verb from verb array + */ + Verb peek() const { + return (fIter != fEnd) ? static_cast(std::get<0>(*fIter)) : kDone_Verb; + } + + /** Returns conic weight if next() returned kConic_Verb. + + If next() has not been called, or next() did not return kConic_Verb, + result is undefined. + + @return conic weight for conic SkPoint returned by next() + */ + SkScalar conicWeight() const { + return fConicWeight; + } + + private: + RangeIter fIter; + RangeIter fEnd; + SkScalar fConicWeight = 0; + friend class SkPath; + + }; + + /** Returns true if the point (x, y) is contained by SkPath, taking into + account FillType. + + @param x x-axis value of containment test + @param y y-axis value of containment test + @return true if SkPoint is in SkPath + + example: https://fiddle.skia.org/c/@Path_contains + */ + bool contains(SkScalar x, SkScalar y) const; + + /** Writes text representation of SkPath to stream. If stream is nullptr, writes to + standard output. Set dumpAsHex true to generate exact binary representations + of floating point numbers used in SkPoint array and conic weights. + + @param stream writable SkWStream receiving SkPath text representation; may be nullptr + @param dumpAsHex true if SkScalar values are written as hexadecimal + + example: https://fiddle.skia.org/c/@Path_dump + */ + void dump(SkWStream* stream, bool dumpAsHex) const; + + void dump() const { this->dump(nullptr, false); } + void dumpHex() const { this->dump(nullptr, true); } + + // Like dump(), but outputs for the SkPath::Make() factory + void dumpArrays(SkWStream* stream, bool dumpAsHex) const; + void dumpArrays() const { this->dumpArrays(nullptr, false); } + + /** Writes SkPath to buffer, returning the number of bytes written. + Pass nullptr to obtain the storage size. + + Writes SkPath::FillType, verb array, SkPoint array, conic weight, and + additionally writes computed information like SkPath::Convexity and bounds. + + Use only be used in concert with readFromMemory(); + the format used for SkPath in memory is not guaranteed. + + @param buffer storage for SkPath; may be nullptr + @return size of storage required for SkPath; always a multiple of 4 + + example: https://fiddle.skia.org/c/@Path_writeToMemory + */ + size_t writeToMemory(void* buffer) const; + + /** Writes SkPath to buffer, returning the buffer written to, wrapped in SkData. + + serialize() writes SkPath::FillType, verb array, SkPoint array, conic weight, and + additionally writes computed information like SkPath::Convexity and bounds. + + serialize() should only be used in concert with readFromMemory(). + The format used for SkPath in memory is not guaranteed. + + @return SkPath data wrapped in SkData buffer + + example: https://fiddle.skia.org/c/@Path_serialize + */ + sk_sp serialize() const; + + /** Initializes SkPath from buffer of size length. Returns zero if the buffer is + data is inconsistent, or the length is too small. + + Reads SkPath::FillType, verb array, SkPoint array, conic weight, and + additionally reads computed information like SkPath::Convexity and bounds. + + Used only in concert with writeToMemory(); + the format used for SkPath in memory is not guaranteed. + + @param buffer storage for SkPath + @param length buffer size in bytes; must be multiple of 4 + @return number of bytes read, or zero on failure + + example: https://fiddle.skia.org/c/@Path_readFromMemory + */ + size_t readFromMemory(const void* buffer, size_t length); + + /** (See Skia bug 1762.) + Returns a non-zero, globally unique value. A different value is returned + if verb array, SkPoint array, or conic weight changes. + + Setting SkPath::FillType does not change generation identifier. + + Each time the path is modified, a different generation identifier will be returned. + SkPath::FillType does affect generation identifier on Android framework. + + @return non-zero, globally unique value + + example: https://fiddle.skia.org/c/@Path_getGenerationID + */ + uint32_t getGenerationID() const; + + /** Returns if SkPath data is consistent. Corrupt SkPath data is detected if + internal values are out of range or internal storage does not match + array dimensions. + + @return true if SkPath data is consistent + */ + bool isValid() const; + + using sk_is_trivially_relocatable = std::true_type; + +private: + SkPath(sk_sp, SkPathFillType, bool isVolatile, SkPathConvexity, + SkPathFirstDirection firstDirection); + + sk_sp fPathRef; + int fLastMoveToIndex; + mutable std::atomic fConvexity; // SkPathConvexity + mutable std::atomic fFirstDirection; // SkPathFirstDirection + uint8_t fFillType : 2; + uint8_t fIsVolatile : 1; + + static_assert(::sk_is_trivially_relocatable::value); + + /** Resets all fields other than fPathRef to their initial 'empty' values. + * Assumes the caller has already emptied fPathRef. + */ + void resetFields(); + + /** Sets all fields other than fPathRef to the values in 'that'. + * Assumes the caller has already set fPathRef. + * Doesn't change fGenerationID or fSourcePath on Android. + */ + void copyFields(const SkPath& that); + + size_t writeToMemoryAsRRect(void* buffer) const; + size_t readAsRRect(const void*, size_t); + size_t readFromMemory_EQ4Or5(const void*, size_t); + + friend class Iter; + friend class SkPathPriv; + friend class SkPathStroker; + + /* Append, in reverse order, the first contour of path, ignoring path's + last point. If no moveTo() call has been made for this contour, the + first point is automatically set to (0,0). + */ + SkPath& reversePathTo(const SkPath&); + + // called before we add points for lineTo, quadTo, cubicTo, checking to see + // if we need to inject a leading moveTo first + // + // SkPath path; path.lineTo(...); <--- need a leading moveTo(0, 0) + // SkPath path; ... path.close(); path.lineTo(...) <-- need a moveTo(previous moveTo) + // + inline void injectMoveToIfNeeded(); + + inline bool hasOnlyMoveTos() const; + + SkPathConvexity computeConvexity() const; + + bool isValidImpl() const; + /** Asserts if SkPath data is inconsistent. + Debugging check intended for internal use only. + */ +#ifdef SK_DEBUG + void validate() const; + void validateRef() const; +#endif + + // called by stroker to see if all points (in the last contour) are equal and worthy of a cap + bool isZeroLengthSincePoint(int startPtIndex) const; + + /** Returns if the path can return a bound at no cost (true) or will have to + perform some computation (false). + */ + bool hasComputedBounds() const; + + // 'rect' needs to be sorted + void setBounds(const SkRect& rect); + + void setPt(int index, SkScalar x, SkScalar y); + + SkPath& dirtyAfterEdit(); + + // Bottlenecks for working with fConvexity and fFirstDirection. + // Notice the setters are const... these are mutable atomic fields. + void setConvexity(SkPathConvexity) const; + + void setFirstDirection(SkPathFirstDirection) const; + SkPathFirstDirection getFirstDirection() const; + + /** Returns the comvexity type, computing if needed. Never returns kUnknown. + @return path's convexity type (convex or concave) + */ + SkPathConvexity getConvexity() const; + + SkPathConvexity getConvexityOrUnknown() const; + + // Compares the cached value with a freshly computed one (computeConvexity()) + bool isConvexityAccurate() const; + + /** Stores a convexity type for this path. This is what will be returned if + * getConvexityOrUnknown() is called. If you pass kUnknown, then if getContexityType() + * is called, the real convexity will be computed. + * + * example: https://fiddle.skia.org/c/@Path_setConvexity + */ + void setConvexity(SkPathConvexity convexity); + + /** Shrinks SkPath verb array and SkPoint array storage to discard unused capacity. + * May reduce the heap overhead for SkPath known to be fully constructed. + * + * NOTE: This may relocate the underlying buffers, and thus any Iterators referencing + * this path should be discarded after calling shrinkToFit(). + */ + void shrinkToFit(); + + // Creates a new Path after the supplied arguments have been validated by + // sk_path_analyze_verbs(). + static SkPath MakeInternal(const SkPathVerbAnalysis& analsis, + const SkPoint points[], + const uint8_t verbs[], + int verbCount, + const SkScalar conics[], + SkPathFillType fillType, + bool isVolatile); + + friend class SkAutoPathBoundsUpdate; + friend class SkAutoDisableOvalCheck; + friend class SkAutoDisableDirectionCheck; + friend class SkPathBuilder; + friend class SkPathEdgeIter; + friend class SkPathWriter; + friend class SkOpBuilder; + friend class SkBench_AddPathTest; // perf test reversePathTo + friend class PathTest_Private; // unit test reversePathTo + friend class ForceIsRRect_Private; // unit test isRRect + friend class FuzzPath; // for legacy access to validateRef +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathBuilder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathBuilder.h new file mode 100644 index 00000000000..247c08624c5 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathBuilder.h @@ -0,0 +1,271 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPathBuilder_DEFINED +#define SkPathBuilder_DEFINED + +#include "include/core/SkPath.h" +#include "include/core/SkPathTypes.h" +#include "include/core/SkPoint.h" +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" +#include "include/private/SkPathRef.h" +#include "include/private/base/SkTo.h" + +#include + +class SkRRect; + +class SK_API SkPathBuilder { +public: + SkPathBuilder(); + SkPathBuilder(SkPathFillType); + SkPathBuilder(const SkPath&); + SkPathBuilder(const SkPathBuilder&) = default; + ~SkPathBuilder(); + + SkPathBuilder& operator=(const SkPath&); + SkPathBuilder& operator=(const SkPathBuilder&) = default; + + SkPathFillType fillType() const { return fFillType; } + SkRect computeBounds() const; + + SkPath snapshot() const; // the builder is unchanged after returning this path + SkPath detach(); // the builder is reset to empty after returning this path + + SkPathBuilder& setFillType(SkPathFillType ft) { fFillType = ft; return *this; } + SkPathBuilder& setIsVolatile(bool isVolatile) { fIsVolatile = isVolatile; return *this; } + + SkPathBuilder& reset(); + + SkPathBuilder& moveTo(SkPoint pt); + SkPathBuilder& moveTo(SkScalar x, SkScalar y) { return this->moveTo(SkPoint::Make(x, y)); } + + SkPathBuilder& lineTo(SkPoint pt); + SkPathBuilder& lineTo(SkScalar x, SkScalar y) { return this->lineTo(SkPoint::Make(x, y)); } + + SkPathBuilder& quadTo(SkPoint pt1, SkPoint pt2); + SkPathBuilder& quadTo(SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2) { + return this->quadTo(SkPoint::Make(x1, y1), SkPoint::Make(x2, y2)); + } + SkPathBuilder& quadTo(const SkPoint pts[2]) { return this->quadTo(pts[0], pts[1]); } + + SkPathBuilder& conicTo(SkPoint pt1, SkPoint pt2, SkScalar w); + SkPathBuilder& conicTo(SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2, SkScalar w) { + return this->conicTo(SkPoint::Make(x1, y1), SkPoint::Make(x2, y2), w); + } + SkPathBuilder& conicTo(const SkPoint pts[2], SkScalar w) { + return this->conicTo(pts[0], pts[1], w); + } + + SkPathBuilder& cubicTo(SkPoint pt1, SkPoint pt2, SkPoint pt3); + SkPathBuilder& cubicTo(SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2, SkScalar x3, SkScalar y3) { + return this->cubicTo(SkPoint::Make(x1, y1), SkPoint::Make(x2, y2), SkPoint::Make(x3, y3)); + } + SkPathBuilder& cubicTo(const SkPoint pts[3]) { + return this->cubicTo(pts[0], pts[1], pts[2]); + } + + SkPathBuilder& close(); + + // Append a series of lineTo(...) + SkPathBuilder& polylineTo(const SkPoint pts[], int count); + SkPathBuilder& polylineTo(const std::initializer_list& list) { + return this->polylineTo(list.begin(), SkToInt(list.size())); + } + + // Relative versions of segments, relative to the previous position. + + SkPathBuilder& rLineTo(SkPoint pt); + SkPathBuilder& rLineTo(SkScalar x, SkScalar y) { return this->rLineTo({x, y}); } + SkPathBuilder& rQuadTo(SkPoint pt1, SkPoint pt2); + SkPathBuilder& rQuadTo(SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2) { + return this->rQuadTo({x1, y1}, {x2, y2}); + } + SkPathBuilder& rConicTo(SkPoint p1, SkPoint p2, SkScalar w); + SkPathBuilder& rConicTo(SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2, SkScalar w) { + return this->rConicTo({x1, y1}, {x2, y2}, w); + } + SkPathBuilder& rCubicTo(SkPoint pt1, SkPoint pt2, SkPoint pt3); + SkPathBuilder& rCubicTo(SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2, SkScalar x3, SkScalar y3) { + return this->rCubicTo({x1, y1}, {x2, y2}, {x3, y3}); + } + + // Arcs + + /** Appends arc to the builder. Arc added is part of ellipse + bounded by oval, from startAngle through sweepAngle. Both startAngle and + sweepAngle are measured in degrees, where zero degrees is aligned with the + positive x-axis, and positive sweeps extends arc clockwise. + + arcTo() adds line connecting the builder's last point to initial arc point if forceMoveTo + is false and the builder is not empty. Otherwise, added contour begins with first point + of arc. Angles greater than -360 and less than 360 are treated modulo 360. + + @param oval bounds of ellipse containing arc + @param startAngleDeg starting angle of arc in degrees + @param sweepAngleDeg sweep, in degrees. Positive is clockwise; treated modulo 360 + @param forceMoveTo true to start a new contour with arc + @return reference to the builder + */ + SkPathBuilder& arcTo(const SkRect& oval, SkScalar startAngleDeg, SkScalar sweepAngleDeg, + bool forceMoveTo); + + /** Appends arc to SkPath, after appending line if needed. Arc is implemented by conic + weighted to describe part of circle. Arc is contained by tangent from + last SkPath point to p1, and tangent from p1 to p2. Arc + is part of circle sized to radius, positioned so it touches both tangent lines. + + If last SkPath SkPoint does not start arc, arcTo() appends connecting line to SkPath. + The length of vector from p1 to p2 does not affect arc. + + Arc sweep is always less than 180 degrees. If radius is zero, or if + tangents are nearly parallel, arcTo() appends line from last SkPath SkPoint to p1. + + arcTo() appends at most one line and one conic. + arcTo() implements the functionality of PostScript arct and HTML Canvas arcTo. + + @param p1 SkPoint common to pair of tangents + @param p2 end of second tangent + @param radius distance from arc to circle center + @return reference to SkPath + */ + SkPathBuilder& arcTo(SkPoint p1, SkPoint p2, SkScalar radius); + + enum ArcSize { + kSmall_ArcSize, //!< smaller of arc pair + kLarge_ArcSize, //!< larger of arc pair + }; + + /** Appends arc to SkPath. Arc is implemented by one or more conic weighted to describe + part of oval with radii (r.fX, r.fY) rotated by xAxisRotate degrees. Arc curves + from last SkPath SkPoint to (xy.fX, xy.fY), choosing one of four possible routes: + clockwise or counterclockwise, + and smaller or larger. + + Arc sweep is always less than 360 degrees. arcTo() appends line to xy if either + radii are zero, or if last SkPath SkPoint equals (xy.fX, xy.fY). arcTo() scales radii r to + fit last SkPath SkPoint and xy if both are greater than zero but too small to describe + an arc. + + arcTo() appends up to four conic curves. + arcTo() implements the functionality of SVG arc, although SVG sweep-flag value is + opposite the integer value of sweep; SVG sweep-flag uses 1 for clockwise, while + kCW_Direction cast to int is zero. + + @param r radii on axes before x-axis rotation + @param xAxisRotate x-axis rotation in degrees; positive values are clockwise + @param largeArc chooses smaller or larger arc + @param sweep chooses clockwise or counterclockwise arc + @param xy end of arc + @return reference to SkPath + */ + SkPathBuilder& arcTo(SkPoint r, SkScalar xAxisRotate, ArcSize largeArc, SkPathDirection sweep, + SkPoint xy); + + /** Appends arc to the builder, as the start of new contour. Arc added is part of ellipse + bounded by oval, from startAngle through sweepAngle. Both startAngle and + sweepAngle are measured in degrees, where zero degrees is aligned with the + positive x-axis, and positive sweeps extends arc clockwise. + + If sweepAngle <= -360, or sweepAngle >= 360; and startAngle modulo 90 is nearly + zero, append oval instead of arc. Otherwise, sweepAngle values are treated + modulo 360, and arc may or may not draw depending on numeric rounding. + + @param oval bounds of ellipse containing arc + @param startAngleDeg starting angle of arc in degrees + @param sweepAngleDeg sweep, in degrees. Positive is clockwise; treated modulo 360 + @return reference to this builder + */ + SkPathBuilder& addArc(const SkRect& oval, SkScalar startAngleDeg, SkScalar sweepAngleDeg); + + // Add a new contour + + SkPathBuilder& addRect(const SkRect&, SkPathDirection, unsigned startIndex); + SkPathBuilder& addOval(const SkRect&, SkPathDirection, unsigned startIndex); + SkPathBuilder& addRRect(const SkRRect&, SkPathDirection, unsigned startIndex); + + SkPathBuilder& addRect(const SkRect& rect, SkPathDirection dir = SkPathDirection::kCW) { + return this->addRect(rect, dir, 0); + } + SkPathBuilder& addOval(const SkRect& rect, SkPathDirection dir = SkPathDirection::kCW) { + // legacy start index: 1 + return this->addOval(rect, dir, 1); + } + SkPathBuilder& addRRect(const SkRRect& rrect, SkPathDirection dir = SkPathDirection::kCW) { + // legacy start indices: 6 (CW) and 7 (CCW) + return this->addRRect(rrect, dir, dir == SkPathDirection::kCW ? 6 : 7); + } + + SkPathBuilder& addCircle(SkScalar center_x, SkScalar center_y, SkScalar radius, + SkPathDirection dir = SkPathDirection::kCW); + + SkPathBuilder& addPolygon(const SkPoint pts[], int count, bool isClosed); + SkPathBuilder& addPolygon(const std::initializer_list& list, bool isClosed) { + return this->addPolygon(list.begin(), SkToInt(list.size()), isClosed); + } + + SkPathBuilder& addPath(const SkPath&); + + // Performance hint, to reserve extra storage for subsequent calls to lineTo, quadTo, etc. + + void incReserve(int extraPtCount, int extraVerbCount); + void incReserve(int extraPtCount) { + this->incReserve(extraPtCount, extraPtCount); + } + + SkPathBuilder& offset(SkScalar dx, SkScalar dy); + + SkPathBuilder& toggleInverseFillType() { + fFillType = (SkPathFillType)((unsigned)fFillType ^ 2); + return *this; + } + +private: + SkPathRef::PointsArray fPts; + SkPathRef::VerbsArray fVerbs; + SkPathRef::ConicWeightsArray fConicWeights; + + SkPathFillType fFillType; + bool fIsVolatile; + + unsigned fSegmentMask; + SkPoint fLastMovePoint; + int fLastMoveIndex; // only needed until SkPath is immutable + bool fNeedsMoveVerb; + + enum IsA { + kIsA_JustMoves, // we only have 0 or more moves + kIsA_MoreThanMoves, // we have verbs other than just move + kIsA_Oval, // we are 0 or more moves followed by an oval + kIsA_RRect, // we are 0 or more moves followed by a rrect + }; + IsA fIsA = kIsA_JustMoves; + int fIsAStart = -1; // tracks direction iff fIsA is not unknown + bool fIsACCW = false; // tracks direction iff fIsA is not unknown + + int countVerbs() const { return fVerbs.size(); } + + // called right before we add a (non-move) verb + void ensureMove() { + fIsA = kIsA_MoreThanMoves; + if (fNeedsMoveVerb) { + this->moveTo(fLastMovePoint); + } + } + + SkPath make(sk_sp) const; + + SkPathBuilder& privateReverseAddPath(const SkPath&); + + friend class SkPathPriv; +}; + +#endif + diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathEffect.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathEffect.h new file mode 100644 index 00000000000..0ebe39f293a --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathEffect.h @@ -0,0 +1,113 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPathEffect_DEFINED +#define SkPathEffect_DEFINED + +#include "include/core/SkFlattenable.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/private/base/SkAPI.h" + +// TODO(kjlubick) update clients and remove this unnecessary #include +#include "include/core/SkPath.h" // IWYU pragma: keep + +#include +#include + +class SkMatrix; +class SkStrokeRec; +struct SkDeserialProcs; +struct SkRect; + +/** \class SkPathEffect + + SkPathEffect is the base class for objects in the SkPaint that affect + the geometry of a drawing primitive before it is transformed by the + canvas' matrix and drawn. + + Dashing is implemented as a subclass of SkPathEffect. +*/ +class SK_API SkPathEffect : public SkFlattenable { +public: + /** + * Returns a patheffect that apples each effect (first and second) to the original path, + * and returns a path with the sum of these. + * + * result = first(path) + second(path) + * + */ + static sk_sp MakeSum(sk_sp first, sk_sp second); + + /** + * Returns a patheffect that applies the inner effect to the path, and then applies the + * outer effect to the result of the inner's. + * + * result = outer(inner(path)) + */ + static sk_sp MakeCompose(sk_sp outer, sk_sp inner); + + static SkFlattenable::Type GetFlattenableType() { + return kSkPathEffect_Type; + } + + // move to base? + + enum DashType { + kNone_DashType, //!< ignores the info parameter + kDash_DashType, //!< fills in all of the info parameter + }; + + struct DashInfo { + DashInfo() : fIntervals(nullptr), fCount(0), fPhase(0) {} + DashInfo(SkScalar* intervals, int32_t count, SkScalar phase) + : fIntervals(intervals), fCount(count), fPhase(phase) {} + + SkScalar* fIntervals; //!< Length of on/off intervals for dashed lines + // Even values represent ons, and odds offs + int32_t fCount; //!< Number of intervals in the dash. Should be even number + SkScalar fPhase; //!< Offset into the dashed interval pattern + // mod the sum of all intervals + }; + + DashType asADash(DashInfo* info) const; + + /** + * Given a src path (input) and a stroke-rec (input and output), apply + * this effect to the src path, returning the new path in dst, and return + * true. If this effect cannot be applied, return false and ignore dst + * and stroke-rec. + * + * The stroke-rec specifies the initial request for stroking (if any). + * The effect can treat this as input only, or it can choose to change + * the rec as well. For example, the effect can decide to change the + * stroke's width or join, or the effect can change the rec from stroke + * to fill (or fill to stroke) in addition to returning a new (dst) path. + * + * If this method returns true, the caller will apply (as needed) the + * resulting stroke-rec to dst and then draw. + */ + bool filterPath(SkPath* dst, const SkPath& src, SkStrokeRec*, const SkRect* cullR) const; + + /** Version of filterPath that can be called when the CTM is known. */ + bool filterPath(SkPath* dst, const SkPath& src, SkStrokeRec*, const SkRect* cullR, + const SkMatrix& ctm) const; + + /** True if this path effect requires a valid CTM */ + bool needsCTM() const; + + static sk_sp Deserialize(const void* data, size_t size, + const SkDeserialProcs* procs = nullptr); + +private: + SkPathEffect() = default; + friend class SkPathEffectBase; + + using INHERITED = SkFlattenable; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathMeasure.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathMeasure.h new file mode 100644 index 00000000000..2c2c36007a9 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathMeasure.h @@ -0,0 +1,95 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPathMeasure_DEFINED +#define SkPathMeasure_DEFINED + +#include "include/core/SkContourMeasure.h" +#include "include/core/SkPoint.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkDebug.h" + +class SkMatrix; +class SkPath; + +class SK_API SkPathMeasure { +public: + SkPathMeasure(); + /** Initialize the pathmeasure with the specified path. The parts of the path that are needed + * are copied, so the client is free to modify/delete the path after this call. + * + * resScale controls the precision of the measure. values > 1 increase the + * precision (and possibly slow down the computation). + */ + SkPathMeasure(const SkPath& path, bool forceClosed, SkScalar resScale = 1); + ~SkPathMeasure(); + + SkPathMeasure(SkPathMeasure&&) = default; + SkPathMeasure& operator=(SkPathMeasure&&) = default; + + /** Reset the pathmeasure with the specified path. The parts of the path that are needed + * are copied, so the client is free to modify/delete the path after this call.. + */ + void setPath(const SkPath*, bool forceClosed); + + /** Return the total length of the current contour, or 0 if no path + is associated (e.g. resetPath(null)) + */ + SkScalar getLength(); + + /** Pins distance to 0 <= distance <= getLength(), and then computes + the corresponding position and tangent. + Returns false if there is no path, or a zero-length path was specified, in which case + position and tangent are unchanged. + */ + [[nodiscard]] bool getPosTan(SkScalar distance, SkPoint* position, SkVector* tangent); + + enum MatrixFlags { + kGetPosition_MatrixFlag = 0x01, + kGetTangent_MatrixFlag = 0x02, + kGetPosAndTan_MatrixFlag = kGetPosition_MatrixFlag | kGetTangent_MatrixFlag + }; + + /** Pins distance to 0 <= distance <= getLength(), and then computes + the corresponding matrix (by calling getPosTan). + Returns false if there is no path, or a zero-length path was specified, in which case + matrix is unchanged. + */ + [[nodiscard]] bool getMatrix(SkScalar distance, SkMatrix* matrix, + MatrixFlags flags = kGetPosAndTan_MatrixFlag); + + /** Given a start and stop distance, return in dst the intervening segment(s). + If the segment is zero-length, return false, else return true. + startD and stopD are pinned to legal values (0..getLength()). If startD > stopD + then return false (and leave dst untouched). + Begin the segment with a moveTo if startWithMoveTo is true + */ + bool getSegment(SkScalar startD, SkScalar stopD, SkPath* dst, bool startWithMoveTo); + + /** Return true if the current contour is closed() + */ + bool isClosed(); + + /** Move to the next contour in the path. Return true if one exists, or false if + we're done with the path. + */ + bool nextContour(); + +#ifdef SK_DEBUG + void dump(); +#endif + + const SkContourMeasure* currentMeasure() const { return fContour.get(); } + +private: + SkContourMeasureIter fIter; + sk_sp fContour; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathTypes.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathTypes.h new file mode 100644 index 00000000000..963a6bda00b --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathTypes.h @@ -0,0 +1,57 @@ +/* + * Copyright 2019 Google LLC. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPathTypes_DEFINED +#define SkPathTypes_DEFINED + +enum class SkPathFillType { + /** Specifies that "inside" is computed by a non-zero sum of signed edge crossings */ + kWinding, + /** Specifies that "inside" is computed by an odd number of edge crossings */ + kEvenOdd, + /** Same as Winding, but draws outside of the path, rather than inside */ + kInverseWinding, + /** Same as EvenOdd, but draws outside of the path, rather than inside */ + kInverseEvenOdd +}; + +static inline bool SkPathFillType_IsEvenOdd(SkPathFillType ft) { + return (static_cast(ft) & 1) != 0; +} + +static inline bool SkPathFillType_IsInverse(SkPathFillType ft) { + return (static_cast(ft) & 2) != 0; +} + +static inline SkPathFillType SkPathFillType_ConvertToNonInverse(SkPathFillType ft) { + return static_cast(static_cast(ft) & 1); +} + +enum class SkPathDirection { + /** clockwise direction for adding closed contours */ + kCW, + /** counter-clockwise direction for adding closed contours */ + kCCW, +}; + +enum SkPathSegmentMask { + kLine_SkPathSegmentMask = 1 << 0, + kQuad_SkPathSegmentMask = 1 << 1, + kConic_SkPathSegmentMask = 1 << 2, + kCubic_SkPathSegmentMask = 1 << 3, +}; + +enum class SkPathVerb { + kMove, //!< SkPath::RawIter returns 1 point + kLine, //!< SkPath::RawIter returns 2 points + kQuad, //!< SkPath::RawIter returns 3 points + kConic, //!< SkPath::RawIter returns 3 points + 1 weight + kCubic, //!< SkPath::RawIter returns 4 points + kClose //!< SkPath::RawIter returns 0 points +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathUtils.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathUtils.h new file mode 100644 index 00000000000..6285da79960 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPathUtils.h @@ -0,0 +1,42 @@ +/* + * Copyright 2022 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkPathUtils_DEFINED +#define SkPathUtils_DEFINED + +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" + +class SkMatrix; +class SkPaint; +class SkPath; +struct SkRect; + +namespace skpathutils { + +/** Returns the filled equivalent of the stroked path. + + @param src SkPath read to create a filled version + @param paint SkPaint, from which attributes such as stroke cap, width, miter, and join, + as well as pathEffect will be used. + @param dst resulting SkPath; may be the same as src, but may not be nullptr + @param cullRect optional limit passed to SkPathEffect + @param resScale if > 1, increase precision, else if (0 < resScale < 1) reduce precision + to favor speed and size + @return true if the dst path was updated, false if it was not (e.g. if the path + represents hairline and cannot be filled). +*/ +SK_API bool FillPathWithPaint(const SkPath &src, const SkPaint &paint, SkPath *dst, + const SkRect *cullRect, SkScalar resScale = 1); + +SK_API bool FillPathWithPaint(const SkPath &src, const SkPaint &paint, SkPath *dst, + const SkRect *cullRect, const SkMatrix &ctm); + +SK_API bool FillPathWithPaint(const SkPath &src, const SkPaint &paint, SkPath *dst); + +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPicture.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPicture.h new file mode 100644 index 00000000000..f4ab3ed29dd --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPicture.h @@ -0,0 +1,291 @@ +/* + * Copyright 2007 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPicture_DEFINED +#define SkPicture_DEFINED + +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkShader.h" // IWYU pragma: keep +#include "include/core/SkTypes.h" + +#include +#include +#include + +class SkCanvas; +class SkData; +class SkMatrix; +class SkStream; +class SkWStream; +enum class SkFilterMode; +struct SkDeserialProcs; +struct SkSerialProcs; + +// TODO(kjlubick) Remove this after cleaning up clients +#include "include/core/SkTileMode.h" // IWYU pragma: keep + +/** \class SkPicture + SkPicture records drawing commands made to SkCanvas. The command stream may be + played in whole or in part at a later time. + + SkPicture is an abstract class. SkPicture may be generated by SkPictureRecorder + or SkDrawable, or from SkPicture previously saved to SkData or SkStream. + + SkPicture may contain any SkCanvas drawing command, as well as one or more + SkCanvas matrix or SkCanvas clip. SkPicture has a cull SkRect, which is used as + a bounding box hint. To limit SkPicture bounds, use SkCanvas clip when + recording or drawing SkPicture. +*/ +class SK_API SkPicture : public SkRefCnt { +public: + ~SkPicture() override; + + /** Recreates SkPicture that was serialized into a stream. Returns constructed SkPicture + if successful; otherwise, returns nullptr. Fails if data does not permit + constructing valid SkPicture. + + procs->fPictureProc permits supplying a custom function to decode SkPicture. + If procs->fPictureProc is nullptr, default decoding is used. procs->fPictureCtx + may be used to provide user context to procs->fPictureProc; procs->fPictureProc + is called with a pointer to data, data byte length, and user context. + + @param stream container for serial data + @param procs custom serial data decoders; may be nullptr + @return SkPicture constructed from stream data + */ + static sk_sp MakeFromStream(SkStream* stream, + const SkDeserialProcs* procs = nullptr); + + /** Recreates SkPicture that was serialized into data. Returns constructed SkPicture + if successful; otherwise, returns nullptr. Fails if data does not permit + constructing valid SkPicture. + + procs->fPictureProc permits supplying a custom function to decode SkPicture. + If procs->fPictureProc is nullptr, default decoding is used. procs->fPictureCtx + may be used to provide user context to procs->fPictureProc; procs->fPictureProc + is called with a pointer to data, data byte length, and user context. + + @param data container for serial data + @param procs custom serial data decoders; may be nullptr + @return SkPicture constructed from data + */ + static sk_sp MakeFromData(const SkData* data, + const SkDeserialProcs* procs = nullptr); + + /** + + @param data pointer to serial data + @param size size of data + @param procs custom serial data decoders; may be nullptr + @return SkPicture constructed from data + */ + static sk_sp MakeFromData(const void* data, size_t size, + const SkDeserialProcs* procs = nullptr); + + /** \class SkPicture::AbortCallback + AbortCallback is an abstract class. An implementation of AbortCallback may + passed as a parameter to SkPicture::playback, to stop it before all drawing + commands have been processed. + + If AbortCallback::abort returns true, SkPicture::playback is interrupted. + */ + class SK_API AbortCallback { + public: + /** Has no effect. + */ + virtual ~AbortCallback() = default; + + /** Stops SkPicture playback when some condition is met. A subclass of + AbortCallback provides an override for abort() that can stop SkPicture::playback. + + The part of SkPicture drawn when aborted is undefined. SkPicture instantiations are + free to stop drawing at different points during playback. + + If the abort happens inside one or more calls to SkCanvas::save(), stack + of SkCanvas matrix and SkCanvas clip values is restored to its state before + SkPicture::playback was called. + + @return true to stop playback + + example: https://fiddle.skia.org/c/@Picture_AbortCallback_abort + */ + virtual bool abort() = 0; + + protected: + AbortCallback() = default; + AbortCallback(const AbortCallback&) = delete; + AbortCallback& operator=(const AbortCallback&) = delete; + }; + + /** Replays the drawing commands on the specified canvas. In the case that the + commands are recorded, each command in the SkPicture is sent separately to canvas. + + To add a single command to draw SkPicture to recording canvas, call + SkCanvas::drawPicture instead. + + @param canvas receiver of drawing commands + @param callback allows interruption of playback + + example: https://fiddle.skia.org/c/@Picture_playback + */ + virtual void playback(SkCanvas* canvas, AbortCallback* callback = nullptr) const = 0; + + /** Returns cull SkRect for this picture, passed in when SkPicture was created. + Returned SkRect does not specify clipping SkRect for SkPicture; cull is hint + of SkPicture bounds. + + SkPicture is free to discard recorded drawing commands that fall outside + cull. + + @return bounds passed when SkPicture was created + + example: https://fiddle.skia.org/c/@Picture_cullRect + */ + virtual SkRect cullRect() const = 0; + + /** Returns a non-zero value unique among SkPicture in Skia process. + + @return identifier for SkPicture + */ + uint32_t uniqueID() const { return fUniqueID; } + + /** Returns storage containing SkData describing SkPicture, using optional custom + encoders. + + procs->fPictureProc permits supplying a custom function to encode SkPicture. + If procs->fPictureProc is nullptr, default encoding is used. procs->fPictureCtx + may be used to provide user context to procs->fPictureProc; procs->fPictureProc + is called with a pointer to SkPicture and user context. + + The default behavior for serializing SkImages is to encode a nullptr. Should + clients want to, for example, encode these SkImages as PNGs so they can be + deserialized, they must provide SkSerialProcs with the fImageProc set to do so. + + @param procs custom serial data encoders; may be nullptr + @return storage containing serialized SkPicture + + example: https://fiddle.skia.org/c/@Picture_serialize + */ + sk_sp serialize(const SkSerialProcs* procs = nullptr) const; + + /** Writes picture to stream, using optional custom encoders. + + procs->fPictureProc permits supplying a custom function to encode SkPicture. + If procs->fPictureProc is nullptr, default encoding is used. procs->fPictureCtx + may be used to provide user context to procs->fPictureProc; procs->fPictureProc + is called with a pointer to SkPicture and user context. + + The default behavior for serializing SkImages is to encode a nullptr. Should + clients want to, for example, encode these SkImages as PNGs so they can be + deserialized, they must provide SkSerialProcs with the fImageProc set to do so. + + @param stream writable serial data stream + @param procs custom serial data encoders; may be nullptr + + example: https://fiddle.skia.org/c/@Picture_serialize_2 + */ + void serialize(SkWStream* stream, const SkSerialProcs* procs = nullptr) const; + + /** Returns a placeholder SkPicture. Result does not draw, and contains only + cull SkRect, a hint of its bounds. Result is immutable; it cannot be changed + later. Result identifier is unique. + + Returned placeholder can be intercepted during playback to insert other + commands into SkCanvas draw stream. + + @param cull placeholder dimensions + @return placeholder with unique identifier + + example: https://fiddle.skia.org/c/@Picture_MakePlaceholder + */ + static sk_sp MakePlaceholder(SkRect cull); + + /** Returns the approximate number of operations in SkPicture. Returned value + may be greater or less than the number of SkCanvas calls + recorded: some calls may be recorded as more than one operation, other + calls may be optimized away. + + @param nested if true, include the op-counts of nested pictures as well, else + just return count the ops in the top-level picture. + @return approximate operation count + + example: https://fiddle.skia.org/c/@Picture_approximateOpCount + */ + virtual int approximateOpCount(bool nested = false) const = 0; + + /** Returns the approximate byte size of SkPicture. Does not include large objects + referenced by SkPicture. + + @return approximate size + + example: https://fiddle.skia.org/c/@Picture_approximateBytesUsed + */ + virtual size_t approximateBytesUsed() const = 0; + + /** Return a new shader that will draw with this picture. + * + * @param tmx The tiling mode to use when sampling in the x-direction. + * @param tmy The tiling mode to use when sampling in the y-direction. + * @param mode How to filter the tiles + * @param localMatrix Optional matrix used when sampling + * @param tileRect The tile rectangle in picture coordinates: this represents the subset + * (or superset) of the picture used when building a tile. It is not + * affected by localMatrix and does not imply scaling (only translation + * and cropping). If null, the tile rect is considered equal to the picture + * bounds. + * @return Returns a new shader object. Note: this function never returns null. + */ + sk_sp makeShader(SkTileMode tmx, SkTileMode tmy, SkFilterMode mode, + const SkMatrix* localMatrix, const SkRect* tileRect) const; + + sk_sp makeShader(SkTileMode tmx, SkTileMode tmy, SkFilterMode mode) const { + return this->makeShader(tmx, tmy, mode, nullptr, nullptr); + } + +private: + // Allowed subclasses. + SkPicture(); + friend class SkBigPicture; + friend class SkEmptyPicture; + friend class SkPicturePriv; + + void serialize(SkWStream*, const SkSerialProcs*, class SkRefCntSet* typefaces, + bool textBlobsOnly=false) const; + static sk_sp MakeFromStreamPriv(SkStream*, const SkDeserialProcs*, + class SkTypefacePlayback*, + int recursionLimit); + friend class SkPictureData; + + /** Return true if the SkStream/Buffer represents a serialized picture, and + fills out SkPictInfo. After this function returns, the data source is not + rewound so it will have to be manually reset before passing to + MakeFromStream or MakeFromBuffer. Note, MakeFromStream and + MakeFromBuffer perform this check internally so these entry points are + intended for stand alone tools. + If false is returned, SkPictInfo is unmodified. + */ + static bool StreamIsSKP(SkStream*, struct SkPictInfo*); + static bool BufferIsSKP(class SkReadBuffer*, struct SkPictInfo*); + friend bool SkPicture_StreamIsSKP(SkStream*, struct SkPictInfo*); + + // Returns NULL if this is not an SkBigPicture. + virtual const class SkBigPicture* asSkBigPicture() const { return nullptr; } + + static bool IsValidPictInfo(const struct SkPictInfo& info); + static sk_sp Forwardport(const struct SkPictInfo&, + const class SkPictureData*, + class SkReadBuffer* buffer); + + struct SkPictInfo createHeader() const; + class SkPictureData* backport() const; + + uint32_t fUniqueID; + mutable std::atomic fAddedToCache{false}; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPictureRecorder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPictureRecorder.h new file mode 100644 index 00000000000..573a643f734 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPictureRecorder.h @@ -0,0 +1,115 @@ +/* + * Copyright 2014 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPictureRecorder_DEFINED +#define SkPictureRecorder_DEFINED + +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/private/base/SkAPI.h" + +#include + +#ifdef SK_BUILD_FOR_ANDROID_FRAMEWORK +namespace android { + class Picture; +}; +#endif + +class SkBBHFactory; +class SkBBoxHierarchy; +class SkCanvas; +class SkDrawable; +class SkPicture; +class SkRecord; +class SkRecorder; + +class SK_API SkPictureRecorder { +public: + SkPictureRecorder(); + ~SkPictureRecorder(); + + /** Returns the canvas that records the drawing commands. + @param bounds the cull rect used when recording this picture. Any drawing the falls outside + of this rect is undefined, and may be drawn or it may not. + @param bbh optional acceleration structure + @param recordFlags optional flags that control recording. + @return the canvas. + */ + SkCanvas* beginRecording(const SkRect& bounds, sk_sp bbh); + + SkCanvas* beginRecording(const SkRect& bounds, SkBBHFactory* bbhFactory = nullptr); + + SkCanvas* beginRecording(SkScalar width, SkScalar height, + SkBBHFactory* bbhFactory = nullptr) { + return this->beginRecording(SkRect::MakeWH(width, height), bbhFactory); + } + + /** Returns the recording canvas if one is active, or NULL if recording is + not active. This does not alter the refcnt on the canvas (if present). + */ + SkCanvas* getRecordingCanvas(); + + /** + * Signal that the caller is done recording. This invalidates the canvas returned by + * beginRecording/getRecordingCanvas. Ownership of the object is passed to the caller, who + * must call unref() when they are done using it. + * + * The returned picture is immutable. If during recording drawables were added to the canvas, + * these will have been "drawn" into a recording canvas, so that this resulting picture will + * reflect their current state, but will not contain a live reference to the drawables + * themselves. + */ + sk_sp finishRecordingAsPicture(); + + /** + * Signal that the caller is done recording, and update the cull rect to use for bounding + * box hierarchy (BBH) generation. The behavior is the same as calling + * finishRecordingAsPicture(), except that this method updates the cull rect initially passed + * into beginRecording. + * @param cullRect the new culling rectangle to use as the overall bound for BBH generation + * and subsequent culling operations. + * @return the picture containing the recorded content. + */ + sk_sp finishRecordingAsPictureWithCull(const SkRect& cullRect); + + /** + * Signal that the caller is done recording. This invalidates the canvas returned by + * beginRecording/getRecordingCanvas. Ownership of the object is passed to the caller, who + * must call unref() when they are done using it. + * + * Unlike finishRecordingAsPicture(), which returns an immutable picture, the returned drawable + * may contain live references to other drawables (if they were added to the recording canvas) + * and therefore this drawable will reflect the current state of those nested drawables anytime + * it is drawn or a new picture is snapped from it (by calling drawable->makePictureSnapshot()). + */ + sk_sp finishRecordingAsDrawable(); + +private: + void reset(); + + /** Replay the current (partially recorded) operation stream into + canvas. This call doesn't close the current recording. + */ +#ifdef SK_BUILD_FOR_ANDROID_FRAMEWORK + friend class android::Picture; +#endif + friend class SkPictureRecorderReplayTester; // for unit testing + void partialReplay(SkCanvas* canvas) const; + + bool fActivelyRecording; + SkRect fCullRect; + sk_sp fBBH; + std::unique_ptr fRecorder; + sk_sp fRecord; + + SkPictureRecorder(SkPictureRecorder&&) = delete; + SkPictureRecorder& operator=(SkPictureRecorder&&) = delete; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPixelRef.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPixelRef.h new file mode 100644 index 00000000000..12779890f88 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPixelRef.h @@ -0,0 +1,119 @@ +/* + * Copyright 2008 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPixelRef_DEFINED +#define SkPixelRef_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkSize.h" +#include "include/private/SkIDChangeListener.h" +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkTo.h" + +#include +#include +#include + +class SkDiscardableMemory; + +/** \class SkPixelRef + + This class is the smart container for pixel memory, and is used with SkBitmap. + This class can be shared/accessed between multiple threads. +*/ +class SK_API SkPixelRef : public SkRefCnt { +public: + SkPixelRef(int width, int height, void* addr, size_t rowBytes); + ~SkPixelRef() override; + + SkISize dimensions() const { return {fWidth, fHeight}; } + int width() const { return fWidth; } + int height() const { return fHeight; } + void* pixels() const { return fPixels; } + size_t rowBytes() const { return fRowBytes; } + + /** Returns a non-zero, unique value corresponding to the pixels in this + pixelref. Each time the pixels are changed (and notifyPixelsChanged is + called), a different generation ID will be returned. + */ + uint32_t getGenerationID() const; + + /** + * Call this if you have changed the contents of the pixels. This will in- + * turn cause a different generation ID value to be returned from + * getGenerationID(). + */ + void notifyPixelsChanged(); + + /** Returns true if this pixelref is marked as immutable, meaning that the + contents of its pixels will not change for the lifetime of the pixelref. + */ + bool isImmutable() const { return fMutability != kMutable; } + + /** Marks this pixelref is immutable, meaning that the contents of its + pixels will not change for the lifetime of the pixelref. This state can + be set on a pixelref, but it cannot be cleared once it is set. + */ + void setImmutable(); + + // Register a listener that may be called the next time our generation ID changes. + // + // We'll only call the listener if we're confident that we are the only SkPixelRef with this + // generation ID. If our generation ID changes and we decide not to call the listener, we'll + // never call it: you must add a new listener for each generation ID change. We also won't call + // the listener when we're certain no one knows what our generation ID is. + // + // This can be used to invalidate caches keyed by SkPixelRef generation ID. + // Takes ownership of listener. Threadsafe. + void addGenIDChangeListener(sk_sp listener); + + // Call when this pixelref is part of the key to a resourcecache entry. This allows the cache + // to know automatically those entries can be purged when this pixelref is changed or deleted. + void notifyAddedToCache() { + fAddedToCache.store(true); + } + + virtual SkDiscardableMemory* diagnostic_only_getDiscardable() const { return nullptr; } + +protected: + void android_only_reset(int width, int height, size_t rowBytes); + +private: + int fWidth; + int fHeight; + void* fPixels; + size_t fRowBytes; + + // Bottom bit indicates the Gen ID is unique. + bool genIDIsUnique() const { return SkToBool(fTaggedGenID.load() & 1); } + mutable std::atomic fTaggedGenID; + + SkIDChangeListener::List fGenIDChangeListeners; + + // Set true by caches when they cache content that's derived from the current pixels. + std::atomic fAddedToCache; + + enum Mutability { + kMutable, // PixelRefs begin mutable. + kTemporarilyImmutable, // Considered immutable, but can revert to mutable. + kImmutable, // Once set to this state, it never leaves. + } fMutability : 8; // easily fits inside a byte + + void needsNewGenID(); + void callGenIDChangeListeners(); + + void setTemporarilyImmutable(); + void restoreMutability(); + friend class SkSurface_Raster; // For temporary immutable methods above. + + void setImmutableWithID(uint32_t genID); + friend void SkBitmapCache_setImmutableWithID(SkPixelRef*, uint32_t); + + using INHERITED = SkRefCnt; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPixmap.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPixmap.h new file mode 100644 index 00000000000..e3379cbcf9b --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPixmap.h @@ -0,0 +1,731 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPixmap_DEFINED +#define SkPixmap_DEFINED + +#include "include/core/SkColor.h" +#include "include/core/SkColorType.h" +#include "include/core/SkImageInfo.h" +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkSamplingOptions.h" +#include "include/core/SkSize.h" +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkAssert.h" + +#include +#include + +class SkColorSpace; +enum SkAlphaType : int; +struct SkMask; + +/** \class SkPixmap + SkPixmap provides a utility to pair SkImageInfo with pixels and row bytes. + SkPixmap is a low level class which provides convenience functions to access + raster destinations. SkCanvas can not draw SkPixmap, nor does SkPixmap provide + a direct drawing destination. + + Use SkBitmap to draw pixels referenced by SkPixmap; use SkSurface to draw into + pixels referenced by SkPixmap. + + SkPixmap does not try to manage the lifetime of the pixel memory. Use SkPixelRef + to manage pixel memory; SkPixelRef is safe across threads. +*/ +class SK_API SkPixmap { +public: + + /** Creates an empty SkPixmap without pixels, with kUnknown_SkColorType, with + kUnknown_SkAlphaType, and with a width and height of zero. Use + reset() to associate pixels, SkColorType, SkAlphaType, width, and height + after SkPixmap has been created. + + @return empty SkPixmap + */ + SkPixmap() + : fPixels(nullptr), fRowBytes(0), fInfo(SkImageInfo::MakeUnknown(0, 0)) + {} + + /** Creates SkPixmap from info width, height, SkAlphaType, and SkColorType. + addr points to pixels, or nullptr. rowBytes should be info.width() times + info.bytesPerPixel(), or larger. + + No parameter checking is performed; it is up to the caller to ensure that + addr and rowBytes agree with info. + + The memory lifetime of pixels is managed by the caller. When SkPixmap goes + out of scope, addr is unaffected. + + SkPixmap may be later modified by reset() to change its size, pixel type, or + storage. + + @param info width, height, SkAlphaType, SkColorType of SkImageInfo + @param addr pointer to pixels allocated by caller; may be nullptr + @param rowBytes size of one row of addr; width times pixel size, or larger + @return initialized SkPixmap + */ + SkPixmap(const SkImageInfo& info, const void* addr, size_t rowBytes) + : fPixels(addr), fRowBytes(rowBytes), fInfo(info) + {} + + /** Sets width, height, row bytes to zero; pixel address to nullptr; SkColorType to + kUnknown_SkColorType; and SkAlphaType to kUnknown_SkAlphaType. + + The prior pixels are unaffected; it is up to the caller to release pixels + memory if desired. + + example: https://fiddle.skia.org/c/@Pixmap_reset + */ + void reset(); + + /** Sets width, height, SkAlphaType, and SkColorType from info. + Sets pixel address from addr, which may be nullptr. + Sets row bytes from rowBytes, which should be info.width() times + info.bytesPerPixel(), or larger. + + Does not check addr. Asserts if built with SK_DEBUG defined and if rowBytes is + too small to hold one row of pixels. + + The memory lifetime pixels are managed by the caller. When SkPixmap goes + out of scope, addr is unaffected. + + @param info width, height, SkAlphaType, SkColorType of SkImageInfo + @param addr pointer to pixels allocated by caller; may be nullptr + @param rowBytes size of one row of addr; width times pixel size, or larger + + example: https://fiddle.skia.org/c/@Pixmap_reset_2 + */ + void reset(const SkImageInfo& info, const void* addr, size_t rowBytes); + + /** Changes SkColorSpace in SkImageInfo; preserves width, height, SkAlphaType, and + SkColorType in SkImage, and leaves pixel address and row bytes unchanged. + SkColorSpace reference count is incremented. + + @param colorSpace SkColorSpace moved to SkImageInfo + + example: https://fiddle.skia.org/c/@Pixmap_setColorSpace + */ + void setColorSpace(sk_sp colorSpace); + + /** Deprecated. + */ + [[nodiscard]] bool reset(const SkMask& mask); + + /** Sets subset width, height, pixel address to intersection of SkPixmap with area, + if intersection is not empty; and return true. Otherwise, leave subset unchanged + and return false. + + Failing to read the return value generates a compile time warning. + + @param subset storage for width, height, pixel address of intersection + @param area bounds to intersect with SkPixmap + @return true if intersection of SkPixmap and area is not empty + */ + [[nodiscard]] bool extractSubset(SkPixmap* subset, const SkIRect& area) const; + + /** Returns width, height, SkAlphaType, SkColorType, and SkColorSpace. + + @return reference to SkImageInfo + */ + const SkImageInfo& info() const { return fInfo; } + + /** Returns row bytes, the interval from one pixel row to the next. Row bytes + is at least as large as: width() * info().bytesPerPixel(). + + Returns zero if colorType() is kUnknown_SkColorType. + It is up to the SkBitmap creator to ensure that row bytes is a useful value. + + @return byte length of pixel row + */ + size_t rowBytes() const { return fRowBytes; } + + /** Returns pixel address, the base address corresponding to the pixel origin. + + It is up to the SkPixmap creator to ensure that pixel address is a useful value. + + @return pixel address + */ + const void* addr() const { return fPixels; } + + /** Returns pixel count in each pixel row. Should be equal or less than: + rowBytes() / info().bytesPerPixel(). + + @return pixel width in SkImageInfo + */ + int width() const { return fInfo.width(); } + + /** Returns pixel row count. + + @return pixel height in SkImageInfo + */ + int height() const { return fInfo.height(); } + + /** + * Return the dimensions of the pixmap (from its ImageInfo) + */ + SkISize dimensions() const { return fInfo.dimensions(); } + + SkColorType colorType() const { return fInfo.colorType(); } + + SkAlphaType alphaType() const { return fInfo.alphaType(); } + + /** Returns SkColorSpace, the range of colors, associated with SkImageInfo. The + reference count of SkColorSpace is unchanged. The returned SkColorSpace is + immutable. + + @return SkColorSpace in SkImageInfo, or nullptr + */ + SkColorSpace* colorSpace() const; + + /** Returns smart pointer to SkColorSpace, the range of colors, associated with + SkImageInfo. The smart pointer tracks the number of objects sharing this + SkColorSpace reference so the memory is released when the owners destruct. + + The returned SkColorSpace is immutable. + + @return SkColorSpace in SkImageInfo wrapped in a smart pointer + */ + sk_sp refColorSpace() const; + + /** Returns true if SkAlphaType is kOpaque_SkAlphaType. + Does not check if SkColorType allows alpha, or if any pixel value has + transparency. + + @return true if SkImageInfo has opaque SkAlphaType + */ + bool isOpaque() const { return fInfo.isOpaque(); } + + /** Returns SkIRect { 0, 0, width(), height() }. + + @return integral rectangle from origin to width() and height() + */ + SkIRect bounds() const { return SkIRect::MakeWH(this->width(), this->height()); } + + /** Returns number of pixels that fit on row. Should be greater than or equal to + width(). + + @return maximum pixels per row + */ + int rowBytesAsPixels() const { return int(fRowBytes >> this->shiftPerPixel()); } + + /** Returns bit shift converting row bytes to row pixels. + Returns zero for kUnknown_SkColorType. + + @return one of: 0, 1, 2, 3; left shift to convert pixels to bytes + */ + int shiftPerPixel() const { return fInfo.shiftPerPixel(); } + + /** Returns minimum memory required for pixel storage. + Does not include unused memory on last row when rowBytesAsPixels() exceeds width(). + Returns SIZE_MAX if result does not fit in size_t. + Returns zero if height() or width() is 0. + Returns height() times rowBytes() if colorType() is kUnknown_SkColorType. + + @return size in bytes of image buffer + */ + size_t computeByteSize() const { return fInfo.computeByteSize(fRowBytes); } + + /** Returns true if all pixels are opaque. SkColorType determines how pixels + are encoded, and whether pixel describes alpha. Returns true for SkColorType + without alpha in each pixel; for other SkColorType, returns true if all + pixels have alpha values equivalent to 1.0 or greater. + + For SkColorType kRGB_565_SkColorType or kGray_8_SkColorType: always + returns true. For SkColorType kAlpha_8_SkColorType, kBGRA_8888_SkColorType, + kRGBA_8888_SkColorType: returns true if all pixel alpha values are 255. + For SkColorType kARGB_4444_SkColorType: returns true if all pixel alpha values are 15. + For kRGBA_F16_SkColorType: returns true if all pixel alpha values are 1.0 or + greater. + + Returns false for kUnknown_SkColorType. + + @return true if all pixels have opaque values or SkColorType is opaque + + example: https://fiddle.skia.org/c/@Pixmap_computeIsOpaque + */ + bool computeIsOpaque() const; + + /** Returns pixel at (x, y) as unpremultiplied color. + Returns black with alpha if SkColorType is kAlpha_8_SkColorType. + + Input is not validated: out of bounds values of x or y trigger an assert() if + built with SK_DEBUG defined; and returns undefined values or may crash if + SK_RELEASE is defined. Fails if SkColorType is kUnknown_SkColorType or + pixel address is nullptr. + + SkColorSpace in SkImageInfo is ignored. Some color precision may be lost in the + conversion to unpremultiplied color; original pixel data may have additional + precision. + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return pixel converted to unpremultiplied color + + example: https://fiddle.skia.org/c/@Pixmap_getColor + */ + SkColor getColor(int x, int y) const; + + /** Returns pixel at (x, y) as unpremultiplied color as an SkColor4f. + Returns black with alpha if SkColorType is kAlpha_8_SkColorType. + + Input is not validated: out of bounds values of x or y trigger an assert() if + built with SK_DEBUG defined; and returns undefined values or may crash if + SK_RELEASE is defined. Fails if SkColorType is kUnknown_SkColorType or + pixel address is nullptr. + + SkColorSpace in SkImageInfo is ignored. Some color precision may be lost in the + conversion to unpremultiplied color; original pixel data may have additional + precision, though this is less likely than for getColor(). Rounding errors may + occur if the underlying type has lower precision. + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return pixel converted to unpremultiplied float color + */ + SkColor4f getColor4f(int x, int y) const; + + /** Look up the pixel at (x,y) and return its alpha component, normalized to [0..1]. + This is roughly equivalent to SkGetColorA(getColor()), but can be more efficent + (and more precise if the pixels store more than 8 bits per component). + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return alpha converted to normalized float + */ + float getAlphaf(int x, int y) const; + + /** Returns readable pixel address at (x, y). Returns nullptr if SkPixelRef is nullptr. + + Input is not validated: out of bounds values of x or y trigger an assert() if + built with SK_DEBUG defined. Returns nullptr if SkColorType is kUnknown_SkColorType. + + Performs a lookup of pixel size; for better performance, call + one of: addr8, addr16, addr32, addr64, or addrF16(). + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return readable generic pointer to pixel + */ + const void* addr(int x, int y) const { + return (const char*)fPixels + fInfo.computeOffset(x, y, fRowBytes); + } + + /** Returns readable base pixel address. Result is addressable as unsigned 8-bit bytes. + Will trigger an assert() if SkColorType is not kAlpha_8_SkColorType or + kGray_8_SkColorType, and is built with SK_DEBUG defined. + + One byte corresponds to one pixel. + + @return readable unsigned 8-bit pointer to pixels + */ + const uint8_t* addr8() const { + SkASSERT(1 == fInfo.bytesPerPixel()); + return reinterpret_cast(fPixels); + } + + /** Returns readable base pixel address. Result is addressable as unsigned 16-bit words. + Will trigger an assert() if SkColorType is not kRGB_565_SkColorType or + kARGB_4444_SkColorType, and is built with SK_DEBUG defined. + + One word corresponds to one pixel. + + @return readable unsigned 16-bit pointer to pixels + */ + const uint16_t* addr16() const { + SkASSERT(2 == fInfo.bytesPerPixel()); + return reinterpret_cast(fPixels); + } + + /** Returns readable base pixel address. Result is addressable as unsigned 32-bit words. + Will trigger an assert() if SkColorType is not kRGBA_8888_SkColorType or + kBGRA_8888_SkColorType, and is built with SK_DEBUG defined. + + One word corresponds to one pixel. + + @return readable unsigned 32-bit pointer to pixels + */ + const uint32_t* addr32() const { + SkASSERT(4 == fInfo.bytesPerPixel()); + return reinterpret_cast(fPixels); + } + + /** Returns readable base pixel address. Result is addressable as unsigned 64-bit words. + Will trigger an assert() if SkColorType is not kRGBA_F16_SkColorType and is built + with SK_DEBUG defined. + + One word corresponds to one pixel. + + @return readable unsigned 64-bit pointer to pixels + */ + const uint64_t* addr64() const { + SkASSERT(8 == fInfo.bytesPerPixel()); + return reinterpret_cast(fPixels); + } + + /** Returns readable base pixel address. Result is addressable as unsigned 16-bit words. + Will trigger an assert() if SkColorType is not kRGBA_F16_SkColorType and is built + with SK_DEBUG defined. + + Each word represents one color component encoded as a half float. + Four words correspond to one pixel. + + @return readable unsigned 16-bit pointer to first component of pixels + */ + const uint16_t* addrF16() const { + SkASSERT(8 == fInfo.bytesPerPixel()); + SkASSERT(kRGBA_F16_SkColorType == fInfo.colorType() || + kRGBA_F16Norm_SkColorType == fInfo.colorType()); + return reinterpret_cast(fPixels); + } + + /** Returns readable pixel address at (x, y). + + Input is not validated: out of bounds values of x or y trigger an assert() if + built with SK_DEBUG defined. + + Will trigger an assert() if SkColorType is not kAlpha_8_SkColorType or + kGray_8_SkColorType, and is built with SK_DEBUG defined. + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return readable unsigned 8-bit pointer to pixel at (x, y) + */ + const uint8_t* addr8(int x, int y) const { + SkASSERT((unsigned)x < (unsigned)fInfo.width()); + SkASSERT((unsigned)y < (unsigned)fInfo.height()); + return (const uint8_t*)((const char*)this->addr8() + (size_t)y * fRowBytes + (x << 0)); + } + + /** Returns readable pixel address at (x, y). + + Input is not validated: out of bounds values of x or y trigger an assert() if + built with SK_DEBUG defined. + + Will trigger an assert() if SkColorType is not kRGB_565_SkColorType or + kARGB_4444_SkColorType, and is built with SK_DEBUG defined. + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return readable unsigned 16-bit pointer to pixel at (x, y) + */ + const uint16_t* addr16(int x, int y) const { + SkASSERT((unsigned)x < (unsigned)fInfo.width()); + SkASSERT((unsigned)y < (unsigned)fInfo.height()); + return (const uint16_t*)((const char*)this->addr16() + (size_t)y * fRowBytes + (x << 1)); + } + + /** Returns readable pixel address at (x, y). + + Input is not validated: out of bounds values of x or y trigger an assert() if + built with SK_DEBUG defined. + + Will trigger an assert() if SkColorType is not kRGBA_8888_SkColorType or + kBGRA_8888_SkColorType, and is built with SK_DEBUG defined. + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return readable unsigned 32-bit pointer to pixel at (x, y) + */ + const uint32_t* addr32(int x, int y) const { + SkASSERT((unsigned)x < (unsigned)fInfo.width()); + SkASSERT((unsigned)y < (unsigned)fInfo.height()); + return (const uint32_t*)((const char*)this->addr32() + (size_t)y * fRowBytes + (x << 2)); + } + + /** Returns readable pixel address at (x, y). + + Input is not validated: out of bounds values of x or y trigger an assert() if + built with SK_DEBUG defined. + + Will trigger an assert() if SkColorType is not kRGBA_F16_SkColorType and is built + with SK_DEBUG defined. + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return readable unsigned 64-bit pointer to pixel at (x, y) + */ + const uint64_t* addr64(int x, int y) const { + SkASSERT((unsigned)x < (unsigned)fInfo.width()); + SkASSERT((unsigned)y < (unsigned)fInfo.height()); + return (const uint64_t*)((const char*)this->addr64() + (size_t)y * fRowBytes + (x << 3)); + } + + /** Returns readable pixel address at (x, y). + + Input is not validated: out of bounds values of x or y trigger an assert() if + built with SK_DEBUG defined. + + Will trigger an assert() if SkColorType is not kRGBA_F16_SkColorType and is built + with SK_DEBUG defined. + + Each unsigned 16-bit word represents one color component encoded as a half float. + Four words correspond to one pixel. + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return readable unsigned 16-bit pointer to pixel component at (x, y) + */ + const uint16_t* addrF16(int x, int y) const { + SkASSERT(kRGBA_F16_SkColorType == fInfo.colorType() || + kRGBA_F16Norm_SkColorType == fInfo.colorType()); + return reinterpret_cast(this->addr64(x, y)); + } + + /** Returns writable base pixel address. + + @return writable generic base pointer to pixels + */ + void* writable_addr() const { return const_cast(fPixels); } + + /** Returns writable pixel address at (x, y). + + Input is not validated: out of bounds values of x or y trigger an assert() if + built with SK_DEBUG defined. Returns zero if SkColorType is kUnknown_SkColorType. + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return writable generic pointer to pixel + */ + void* writable_addr(int x, int y) const { + return const_cast(this->addr(x, y)); + } + + /** Returns writable pixel address at (x, y). Result is addressable as unsigned + 8-bit bytes. Will trigger an assert() if SkColorType is not kAlpha_8_SkColorType + or kGray_8_SkColorType, and is built with SK_DEBUG defined. + + One byte corresponds to one pixel. + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return writable unsigned 8-bit pointer to pixels + */ + uint8_t* writable_addr8(int x, int y) const { + return const_cast(this->addr8(x, y)); + } + + /** Returns writable_addr pixel address at (x, y). Result is addressable as unsigned + 16-bit words. Will trigger an assert() if SkColorType is not kRGB_565_SkColorType + or kARGB_4444_SkColorType, and is built with SK_DEBUG defined. + + One word corresponds to one pixel. + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return writable unsigned 16-bit pointer to pixel + */ + uint16_t* writable_addr16(int x, int y) const { + return const_cast(this->addr16(x, y)); + } + + /** Returns writable pixel address at (x, y). Result is addressable as unsigned + 32-bit words. Will trigger an assert() if SkColorType is not + kRGBA_8888_SkColorType or kBGRA_8888_SkColorType, and is built with SK_DEBUG + defined. + + One word corresponds to one pixel. + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return writable unsigned 32-bit pointer to pixel + */ + uint32_t* writable_addr32(int x, int y) const { + return const_cast(this->addr32(x, y)); + } + + /** Returns writable pixel address at (x, y). Result is addressable as unsigned + 64-bit words. Will trigger an assert() if SkColorType is not + kRGBA_F16_SkColorType and is built with SK_DEBUG defined. + + One word corresponds to one pixel. + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return writable unsigned 64-bit pointer to pixel + */ + uint64_t* writable_addr64(int x, int y) const { + return const_cast(this->addr64(x, y)); + } + + /** Returns writable pixel address at (x, y). Result is addressable as unsigned + 16-bit words. Will trigger an assert() if SkColorType is not + kRGBA_F16_SkColorType and is built with SK_DEBUG defined. + + Each word represents one color component encoded as a half float. + Four words correspond to one pixel. + + @param x column index, zero or greater, and less than width() + @param y row index, zero or greater, and less than height() + @return writable unsigned 16-bit pointer to first component of pixel + */ + uint16_t* writable_addrF16(int x, int y) const { + return reinterpret_cast(writable_addr64(x, y)); + } + + /** Copies a SkRect of pixels to dstPixels. Copy starts at (0, 0), and does not + exceed SkPixmap (width(), height()). + + dstInfo specifies width, height, SkColorType, SkAlphaType, and + SkColorSpace of destination. dstRowBytes specifics the gap from one destination + row to the next. Returns true if pixels are copied. Returns false if + dstInfo address equals nullptr, or dstRowBytes is less than dstInfo.minRowBytes(). + + Pixels are copied only if pixel conversion is possible. If SkPixmap colorType() is + kGray_8_SkColorType, or kAlpha_8_SkColorType; dstInfo.colorType() must match. + If SkPixmap colorType() is kGray_8_SkColorType, dstInfo.colorSpace() must match. + If SkPixmap alphaType() is kOpaque_SkAlphaType, dstInfo.alphaType() must + match. If SkPixmap colorSpace() is nullptr, dstInfo.colorSpace() must match. Returns + false if pixel conversion is not possible. + + Returns false if SkPixmap width() or height() is zero or negative. + + @param dstInfo destination width, height, SkColorType, SkAlphaType, SkColorSpace + @param dstPixels destination pixel storage + @param dstRowBytes destination row length + @return true if pixels are copied to dstPixels + */ + bool readPixels(const SkImageInfo& dstInfo, void* dstPixels, size_t dstRowBytes) const { + return this->readPixels(dstInfo, dstPixels, dstRowBytes, 0, 0); + } + + /** Copies a SkRect of pixels to dstPixels. Copy starts at (srcX, srcY), and does not + exceed SkPixmap (width(), height()). + + dstInfo specifies width, height, SkColorType, SkAlphaType, and + SkColorSpace of destination. dstRowBytes specifics the gap from one destination + row to the next. Returns true if pixels are copied. Returns false if + dstInfo address equals nullptr, or dstRowBytes is less than dstInfo.minRowBytes(). + + Pixels are copied only if pixel conversion is possible. If SkPixmap colorType() is + kGray_8_SkColorType, or kAlpha_8_SkColorType; dstInfo.colorType() must match. + If SkPixmap colorType() is kGray_8_SkColorType, dstInfo.colorSpace() must match. + If SkPixmap alphaType() is kOpaque_SkAlphaType, dstInfo.alphaType() must + match. If SkPixmap colorSpace() is nullptr, dstInfo.colorSpace() must match. Returns + false if pixel conversion is not possible. + + srcX and srcY may be negative to copy only top or left of source. Returns + false if SkPixmap width() or height() is zero or negative. Returns false if: + abs(srcX) >= Pixmap width(), or if abs(srcY) >= Pixmap height(). + + @param dstInfo destination width, height, SkColorType, SkAlphaType, SkColorSpace + @param dstPixels destination pixel storage + @param dstRowBytes destination row length + @param srcX column index whose absolute value is less than width() + @param srcY row index whose absolute value is less than height() + @return true if pixels are copied to dstPixels + */ + bool readPixels(const SkImageInfo& dstInfo, void* dstPixels, size_t dstRowBytes, int srcX, + int srcY) const; + + /** Copies a SkRect of pixels to dst. Copy starts at (srcX, srcY), and does not + exceed SkPixmap (width(), height()). dst specifies width, height, SkColorType, + SkAlphaType, and SkColorSpace of destination. Returns true if pixels are copied. + Returns false if dst address equals nullptr, or dst.rowBytes() is less than + dst SkImageInfo::minRowBytes. + + Pixels are copied only if pixel conversion is possible. If SkPixmap colorType() is + kGray_8_SkColorType, or kAlpha_8_SkColorType; dst.info().colorType must match. + If SkPixmap colorType() is kGray_8_SkColorType, dst.info().colorSpace must match. + If SkPixmap alphaType() is kOpaque_SkAlphaType, dst.info().alphaType must + match. If SkPixmap colorSpace() is nullptr, dst.info().colorSpace must match. Returns + false if pixel conversion is not possible. + + srcX and srcY may be negative to copy only top or left of source. Returns + false SkPixmap width() or height() is zero or negative. Returns false if: + abs(srcX) >= Pixmap width(), or if abs(srcY) >= Pixmap height(). + + @param dst SkImageInfo and pixel address to write to + @param srcX column index whose absolute value is less than width() + @param srcY row index whose absolute value is less than height() + @return true if pixels are copied to dst + */ + bool readPixels(const SkPixmap& dst, int srcX, int srcY) const { + return this->readPixels(dst.info(), dst.writable_addr(), dst.rowBytes(), srcX, srcY); + } + + /** Copies pixels inside bounds() to dst. dst specifies width, height, SkColorType, + SkAlphaType, and SkColorSpace of destination. Returns true if pixels are copied. + Returns false if dst address equals nullptr, or dst.rowBytes() is less than + dst SkImageInfo::minRowBytes. + + Pixels are copied only if pixel conversion is possible. If SkPixmap colorType() is + kGray_8_SkColorType, or kAlpha_8_SkColorType; dst SkColorType must match. + If SkPixmap colorType() is kGray_8_SkColorType, dst SkColorSpace must match. + If SkPixmap alphaType() is kOpaque_SkAlphaType, dst SkAlphaType must + match. If SkPixmap colorSpace() is nullptr, dst SkColorSpace must match. Returns + false if pixel conversion is not possible. + + Returns false if SkPixmap width() or height() is zero or negative. + + @param dst SkImageInfo and pixel address to write to + @return true if pixels are copied to dst + */ + bool readPixels(const SkPixmap& dst) const { + return this->readPixels(dst.info(), dst.writable_addr(), dst.rowBytes(), 0, 0); + } + + /** Copies SkBitmap to dst, scaling pixels to fit dst.width() and dst.height(), and + converting pixels to match dst.colorType() and dst.alphaType(). Returns true if + pixels are copied. Returns false if dst address is nullptr, or dst.rowBytes() is + less than dst SkImageInfo::minRowBytes. + + Pixels are copied only if pixel conversion is possible. If SkPixmap colorType() is + kGray_8_SkColorType, or kAlpha_8_SkColorType; dst SkColorType must match. + If SkPixmap colorType() is kGray_8_SkColorType, dst SkColorSpace must match. + If SkPixmap alphaType() is kOpaque_SkAlphaType, dst SkAlphaType must + match. If SkPixmap colorSpace() is nullptr, dst SkColorSpace must match. Returns + false if pixel conversion is not possible. + + Returns false if SkBitmap width() or height() is zero or negative. + + @param dst SkImageInfo and pixel address to write to + @return true if pixels are scaled to fit dst + + example: https://fiddle.skia.org/c/@Pixmap_scalePixels + */ + bool scalePixels(const SkPixmap& dst, const SkSamplingOptions&) const; + + /** Writes color to pixels bounded by subset; returns true on success. + Returns false if colorType() is kUnknown_SkColorType, or if subset does + not intersect bounds(). + + @param color sRGB unpremultiplied color to write + @param subset bounding integer SkRect of written pixels + @return true if pixels are changed + + example: https://fiddle.skia.org/c/@Pixmap_erase + */ + bool erase(SkColor color, const SkIRect& subset) const; + + /** Writes color to pixels inside bounds(); returns true on success. + Returns false if colorType() is kUnknown_SkColorType, or if bounds() + is empty. + + @param color sRGB unpremultiplied color to write + @return true if pixels are changed + */ + bool erase(SkColor color) const { return this->erase(color, this->bounds()); } + + /** Writes color to pixels bounded by subset; returns true on success. + if subset is nullptr, writes colors pixels inside bounds(). Returns false if + colorType() is kUnknown_SkColorType, if subset is not nullptr and does + not intersect bounds(), or if subset is nullptr and bounds() is empty. + + @param color unpremultiplied color to write + @param subset bounding integer SkRect of pixels to write; may be nullptr + @return true if pixels are changed + */ + bool erase(const SkColor4f& color, const SkIRect* subset = nullptr) const; + +private: + const void* fPixels; + size_t fRowBytes; + SkImageInfo fInfo; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPoint.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPoint.h new file mode 100644 index 00000000000..4c7e0943cd1 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPoint.h @@ -0,0 +1,10 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + */ + +// SkPoint is part of the public API, but is also required by code in base. The following include +// forwarding allows SkPoint to participate in the API and for use by code in base. + +#include "include/private/base/SkPoint_impl.h" // IWYU pragma: export diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPoint3.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPoint3.h new file mode 100644 index 00000000000..abf8dfd9c90 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkPoint3.h @@ -0,0 +1,149 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPoint3_DEFINED +#define SkPoint3_DEFINED + +#include "include/core/SkScalar.h" +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkFloatingPoint.h" + +struct SK_API SkPoint3 { + SkScalar fX, fY, fZ; + + static SkPoint3 Make(SkScalar x, SkScalar y, SkScalar z) { + SkPoint3 pt; + pt.set(x, y, z); + return pt; + } + + SkScalar x() const { return fX; } + SkScalar y() const { return fY; } + SkScalar z() const { return fZ; } + + void set(SkScalar x, SkScalar y, SkScalar z) { fX = x; fY = y; fZ = z; } + + friend bool operator==(const SkPoint3& a, const SkPoint3& b) { + return a.fX == b.fX && a.fY == b.fY && a.fZ == b.fZ; + } + + friend bool operator!=(const SkPoint3& a, const SkPoint3& b) { + return !(a == b); + } + + /** Returns the Euclidian distance from (0,0,0) to (x,y,z) + */ + static SkScalar Length(SkScalar x, SkScalar y, SkScalar z); + + /** Return the Euclidian distance from (0,0,0) to the point + */ + SkScalar length() const { return SkPoint3::Length(fX, fY, fZ); } + + /** Set the point (vector) to be unit-length in the same direction as it + already points. If the point has a degenerate length (i.e., nearly 0) + then set it to (0,0,0) and return false; otherwise return true. + */ + bool normalize(); + + /** Return a new point whose X, Y and Z coordinates are scaled. + */ + SkPoint3 makeScale(SkScalar scale) const { + SkPoint3 p; + p.set(scale * fX, scale * fY, scale * fZ); + return p; + } + + /** Scale the point's coordinates by scale. + */ + void scale(SkScalar value) { + fX *= value; + fY *= value; + fZ *= value; + } + + /** Return a new point whose X, Y and Z coordinates are the negative of the + original point's + */ + SkPoint3 operator-() const { + SkPoint3 neg; + neg.fX = -fX; + neg.fY = -fY; + neg.fZ = -fZ; + return neg; + } + + /** Returns a new point whose coordinates are the difference between + a and b (i.e., a - b) + */ + friend SkPoint3 operator-(const SkPoint3& a, const SkPoint3& b) { + return { a.fX - b.fX, a.fY - b.fY, a.fZ - b.fZ }; + } + + /** Returns a new point whose coordinates are the sum of a and b (a + b) + */ + friend SkPoint3 operator+(const SkPoint3& a, const SkPoint3& b) { + return { a.fX + b.fX, a.fY + b.fY, a.fZ + b.fZ }; + } + + /** Add v's coordinates to the point's + */ + void operator+=(const SkPoint3& v) { + fX += v.fX; + fY += v.fY; + fZ += v.fZ; + } + + /** Subtract v's coordinates from the point's + */ + void operator-=(const SkPoint3& v) { + fX -= v.fX; + fY -= v.fY; + fZ -= v.fZ; + } + + friend SkPoint3 operator*(SkScalar t, SkPoint3 p) { + return { t * p.fX, t * p.fY, t * p.fZ }; + } + + /** Returns true if fX, fY, and fZ are measurable values. + + @return true for values other than infinities and NaN + */ + bool isFinite() const { + return SkIsFinite(fX, fY, fZ); + } + + /** Returns the dot product of a and b, treating them as 3D vectors + */ + static SkScalar DotProduct(const SkPoint3& a, const SkPoint3& b) { + return a.fX * b.fX + a.fY * b.fY + a.fZ * b.fZ; + } + + SkScalar dot(const SkPoint3& vec) const { + return DotProduct(*this, vec); + } + + /** Returns the cross product of a and b, treating them as 3D vectors + */ + static SkPoint3 CrossProduct(const SkPoint3& a, const SkPoint3& b) { + SkPoint3 result; + result.fX = a.fY*b.fZ - a.fZ*b.fY; + result.fY = a.fZ*b.fX - a.fX*b.fZ; + result.fZ = a.fX*b.fY - a.fY*b.fX; + + return result; + } + + SkPoint3 cross(const SkPoint3& vec) const { + return CrossProduct(*this, vec); + } +}; + +typedef SkPoint3 SkVector3; +typedef SkPoint3 SkColor3f; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRRect.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRRect.h new file mode 100644 index 00000000000..b6dc32c5b73 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRRect.h @@ -0,0 +1,516 @@ +/* + * Copyright 2012 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkRRect_DEFINED +#define SkRRect_DEFINED + +#include "include/core/SkPoint.h" +#include "include/core/SkRect.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" + +#include +#include + +class SkMatrix; +class SkString; + +/** \class SkRRect + SkRRect describes a rounded rectangle with a bounds and a pair of radii for each corner. + The bounds and radii can be set so that SkRRect describes: a rectangle with sharp corners; + a circle; an oval; or a rectangle with one or more rounded corners. + + SkRRect allows implementing CSS properties that describe rounded corners. + SkRRect may have up to eight different radii, one for each axis on each of its four + corners. + + SkRRect may modify the provided parameters when initializing bounds and radii. + If either axis radii is zero or less: radii are stored as zero; corner is square. + If corner curves overlap, radii are proportionally reduced to fit within bounds. +*/ +class SK_API SkRRect { +public: + + /** Initializes bounds at (0, 0), the origin, with zero width and height. + Initializes corner radii to (0, 0), and sets type of kEmpty_Type. + + @return empty SkRRect + */ + SkRRect() = default; + + /** Initializes to copy of rrect bounds and corner radii. + + @param rrect bounds and corner to copy + @return copy of rrect + */ + SkRRect(const SkRRect& rrect) = default; + + /** Copies rrect bounds and corner radii. + + @param rrect bounds and corner to copy + @return copy of rrect + */ + SkRRect& operator=(const SkRRect& rrect) = default; + + /** \enum SkRRect::Type + Type describes possible specializations of SkRRect. Each Type is + exclusive; a SkRRect may only have one type. + + Type members become progressively less restrictive; larger values of + Type have more degrees of freedom than smaller values. + */ + enum Type { + kEmpty_Type, //!< zero width or height + kRect_Type, //!< non-zero width and height, and zeroed radii + kOval_Type, //!< non-zero width and height filled with radii + kSimple_Type, //!< non-zero width and height with equal radii + kNinePatch_Type, //!< non-zero width and height with axis-aligned radii + kComplex_Type, //!< non-zero width and height with arbitrary radii + kLastType = kComplex_Type, //!< largest Type value + }; + + Type getType() const { + SkASSERT(this->isValid()); + return static_cast(fType); + } + + Type type() const { return this->getType(); } + + inline bool isEmpty() const { return kEmpty_Type == this->getType(); } + inline bool isRect() const { return kRect_Type == this->getType(); } + inline bool isOval() const { return kOval_Type == this->getType(); } + inline bool isSimple() const { return kSimple_Type == this->getType(); } + inline bool isNinePatch() const { return kNinePatch_Type == this->getType(); } + inline bool isComplex() const { return kComplex_Type == this->getType(); } + + /** Returns span on the x-axis. This does not check if result fits in 32-bit float; + result may be infinity. + + @return rect().fRight minus rect().fLeft + */ + SkScalar width() const { return fRect.width(); } + + /** Returns span on the y-axis. This does not check if result fits in 32-bit float; + result may be infinity. + + @return rect().fBottom minus rect().fTop + */ + SkScalar height() const { return fRect.height(); } + + /** Returns top-left corner radii. If type() returns kEmpty_Type, kRect_Type, + kOval_Type, or kSimple_Type, returns a value representative of all corner radii. + If type() returns kNinePatch_Type or kComplex_Type, at least one of the + remaining three corners has a different value. + + @return corner radii for simple types + */ + SkVector getSimpleRadii() const { + return fRadii[0]; + } + + /** Sets bounds to zero width and height at (0, 0), the origin. Sets + corner radii to zero and sets type to kEmpty_Type. + */ + void setEmpty() { *this = SkRRect(); } + + /** Sets bounds to sorted rect, and sets corner radii to zero. + If set bounds has width and height, and sets type to kRect_Type; + otherwise, sets type to kEmpty_Type. + + @param rect bounds to set + */ + void setRect(const SkRect& rect) { + if (!this->initializeRect(rect)) { + return; + } + + memset(fRadii, 0, sizeof(fRadii)); + fType = kRect_Type; + + SkASSERT(this->isValid()); + } + + /** Initializes bounds at (0, 0), the origin, with zero width and height. + Initializes corner radii to (0, 0), and sets type of kEmpty_Type. + + @return empty SkRRect + */ + static SkRRect MakeEmpty() { return SkRRect(); } + + /** Initializes to copy of r bounds and zeroes corner radii. + + @param r bounds to copy + @return copy of r + */ + static SkRRect MakeRect(const SkRect& r) { + SkRRect rr; + rr.setRect(r); + return rr; + } + + /** Sets bounds to oval, x-axis radii to half oval.width(), and all y-axis radii + to half oval.height(). If oval bounds is empty, sets to kEmpty_Type. + Otherwise, sets to kOval_Type. + + @param oval bounds of oval + @return oval + */ + static SkRRect MakeOval(const SkRect& oval) { + SkRRect rr; + rr.setOval(oval); + return rr; + } + + /** Sets to rounded rectangle with the same radii for all four corners. + If rect is empty, sets to kEmpty_Type. + Otherwise, if xRad and yRad are zero, sets to kRect_Type. + Otherwise, if xRad is at least half rect.width() and yRad is at least half + rect.height(), sets to kOval_Type. + Otherwise, sets to kSimple_Type. + + @param rect bounds of rounded rectangle + @param xRad x-axis radius of corners + @param yRad y-axis radius of corners + @return rounded rectangle + */ + static SkRRect MakeRectXY(const SkRect& rect, SkScalar xRad, SkScalar yRad) { + SkRRect rr; + rr.setRectXY(rect, xRad, yRad); + return rr; + } + + /** Sets bounds to oval, x-axis radii to half oval.width(), and all y-axis radii + to half oval.height(). If oval bounds is empty, sets to kEmpty_Type. + Otherwise, sets to kOval_Type. + + @param oval bounds of oval + */ + void setOval(const SkRect& oval); + + /** Sets to rounded rectangle with the same radii for all four corners. + If rect is empty, sets to kEmpty_Type. + Otherwise, if xRad or yRad is zero, sets to kRect_Type. + Otherwise, if xRad is at least half rect.width() and yRad is at least half + rect.height(), sets to kOval_Type. + Otherwise, sets to kSimple_Type. + + @param rect bounds of rounded rectangle + @param xRad x-axis radius of corners + @param yRad y-axis radius of corners + + example: https://fiddle.skia.org/c/@RRect_setRectXY + */ + void setRectXY(const SkRect& rect, SkScalar xRad, SkScalar yRad); + + /** Sets bounds to rect. Sets radii to (leftRad, topRad), (rightRad, topRad), + (rightRad, bottomRad), (leftRad, bottomRad). + + If rect is empty, sets to kEmpty_Type. + Otherwise, if leftRad and rightRad are zero, sets to kRect_Type. + Otherwise, if topRad and bottomRad are zero, sets to kRect_Type. + Otherwise, if leftRad and rightRad are equal and at least half rect.width(), and + topRad and bottomRad are equal at least half rect.height(), sets to kOval_Type. + Otherwise, if leftRad and rightRad are equal, and topRad and bottomRad are equal, + sets to kSimple_Type. Otherwise, sets to kNinePatch_Type. + + Nine patch refers to the nine parts defined by the radii: one center rectangle, + four edge patches, and four corner patches. + + @param rect bounds of rounded rectangle + @param leftRad left-top and left-bottom x-axis radius + @param topRad left-top and right-top y-axis radius + @param rightRad right-top and right-bottom x-axis radius + @param bottomRad left-bottom and right-bottom y-axis radius + */ + void setNinePatch(const SkRect& rect, SkScalar leftRad, SkScalar topRad, + SkScalar rightRad, SkScalar bottomRad); + + /** Sets bounds to rect. Sets radii array for individual control of all for corners. + + If rect is empty, sets to kEmpty_Type. + Otherwise, if one of each corner radii are zero, sets to kRect_Type. + Otherwise, if all x-axis radii are equal and at least half rect.width(), and + all y-axis radii are equal at least half rect.height(), sets to kOval_Type. + Otherwise, if all x-axis radii are equal, and all y-axis radii are equal, + sets to kSimple_Type. Otherwise, sets to kNinePatch_Type. + + @param rect bounds of rounded rectangle + @param radii corner x-axis and y-axis radii + + example: https://fiddle.skia.org/c/@RRect_setRectRadii + */ + void setRectRadii(const SkRect& rect, const SkVector radii[4]); + + /** \enum SkRRect::Corner + The radii are stored: top-left, top-right, bottom-right, bottom-left. + */ + enum Corner { + kUpperLeft_Corner, //!< index of top-left corner radii + kUpperRight_Corner, //!< index of top-right corner radii + kLowerRight_Corner, //!< index of bottom-right corner radii + kLowerLeft_Corner, //!< index of bottom-left corner radii + }; + + /** Returns bounds. Bounds may have zero width or zero height. Bounds right is + greater than or equal to left; bounds bottom is greater than or equal to top. + Result is identical to getBounds(). + + @return bounding box + */ + const SkRect& rect() const { return fRect; } + + /** Returns scalar pair for radius of curve on x-axis and y-axis for one corner. + Both radii may be zero. If not zero, both are positive and finite. + + @return x-axis and y-axis radii for one corner + */ + SkVector radii(Corner corner) const { return fRadii[corner]; } + + /** Returns bounds. Bounds may have zero width or zero height. Bounds right is + greater than or equal to left; bounds bottom is greater than or equal to top. + Result is identical to rect(). + + @return bounding box + */ + const SkRect& getBounds() const { return fRect; } + + /** Returns true if bounds and radii in a are equal to bounds and radii in b. + + a and b are not equal if either contain NaN. a and b are equal if members + contain zeroes with different signs. + + @param a SkRect bounds and radii to compare + @param b SkRect bounds and radii to compare + @return true if members are equal + */ + friend bool operator==(const SkRRect& a, const SkRRect& b) { + return a.fRect == b.fRect && SkScalarsEqual(&a.fRadii[0].fX, &b.fRadii[0].fX, 8); + } + + /** Returns true if bounds and radii in a are not equal to bounds and radii in b. + + a and b are not equal if either contain NaN. a and b are equal if members + contain zeroes with different signs. + + @param a SkRect bounds and radii to compare + @param b SkRect bounds and radii to compare + @return true if members are not equal + */ + friend bool operator!=(const SkRRect& a, const SkRRect& b) { + return a.fRect != b.fRect || !SkScalarsEqual(&a.fRadii[0].fX, &b.fRadii[0].fX, 8); + } + + /** Copies SkRRect to dst, then insets dst bounds by dx and dy, and adjusts dst + radii by dx and dy. dx and dy may be positive, negative, or zero. dst may be + SkRRect. + + If either corner radius is zero, the corner has no curvature and is unchanged. + Otherwise, if adjusted radius becomes negative, pins radius to zero. + If dx exceeds half dst bounds width, dst bounds left and right are set to + bounds x-axis center. If dy exceeds half dst bounds height, dst bounds top and + bottom are set to bounds y-axis center. + + If dx or dy cause the bounds to become infinite, dst bounds is zeroed. + + @param dx added to rect().fLeft, and subtracted from rect().fRight + @param dy added to rect().fTop, and subtracted from rect().fBottom + @param dst insets bounds and radii + + example: https://fiddle.skia.org/c/@RRect_inset + */ + void inset(SkScalar dx, SkScalar dy, SkRRect* dst) const; + + /** Insets bounds by dx and dy, and adjusts radii by dx and dy. dx and dy may be + positive, negative, or zero. + + If either corner radius is zero, the corner has no curvature and is unchanged. + Otherwise, if adjusted radius becomes negative, pins radius to zero. + If dx exceeds half bounds width, bounds left and right are set to + bounds x-axis center. If dy exceeds half bounds height, bounds top and + bottom are set to bounds y-axis center. + + If dx or dy cause the bounds to become infinite, bounds is zeroed. + + @param dx added to rect().fLeft, and subtracted from rect().fRight + @param dy added to rect().fTop, and subtracted from rect().fBottom + */ + void inset(SkScalar dx, SkScalar dy) { + this->inset(dx, dy, this); + } + + /** Outsets dst bounds by dx and dy, and adjusts radii by dx and dy. dx and dy may be + positive, negative, or zero. + + If either corner radius is zero, the corner has no curvature and is unchanged. + Otherwise, if adjusted radius becomes negative, pins radius to zero. + If dx exceeds half dst bounds width, dst bounds left and right are set to + bounds x-axis center. If dy exceeds half dst bounds height, dst bounds top and + bottom are set to bounds y-axis center. + + If dx or dy cause the bounds to become infinite, dst bounds is zeroed. + + @param dx subtracted from rect().fLeft, and added to rect().fRight + @param dy subtracted from rect().fTop, and added to rect().fBottom + @param dst outset bounds and radii + */ + void outset(SkScalar dx, SkScalar dy, SkRRect* dst) const { + this->inset(-dx, -dy, dst); + } + + /** Outsets bounds by dx and dy, and adjusts radii by dx and dy. dx and dy may be + positive, negative, or zero. + + If either corner radius is zero, the corner has no curvature and is unchanged. + Otherwise, if adjusted radius becomes negative, pins radius to zero. + If dx exceeds half bounds width, bounds left and right are set to + bounds x-axis center. If dy exceeds half bounds height, bounds top and + bottom are set to bounds y-axis center. + + If dx or dy cause the bounds to become infinite, bounds is zeroed. + + @param dx subtracted from rect().fLeft, and added to rect().fRight + @param dy subtracted from rect().fTop, and added to rect().fBottom + */ + void outset(SkScalar dx, SkScalar dy) { + this->inset(-dx, -dy, this); + } + + /** Translates SkRRect by (dx, dy). + + @param dx offset added to rect().fLeft and rect().fRight + @param dy offset added to rect().fTop and rect().fBottom + */ + void offset(SkScalar dx, SkScalar dy) { + fRect.offset(dx, dy); + } + + /** Returns SkRRect translated by (dx, dy). + + @param dx offset added to rect().fLeft and rect().fRight + @param dy offset added to rect().fTop and rect().fBottom + @return SkRRect bounds offset by (dx, dy), with unchanged corner radii + */ + [[nodiscard]] SkRRect makeOffset(SkScalar dx, SkScalar dy) const { + return SkRRect(fRect.makeOffset(dx, dy), fRadii, fType); + } + + /** Returns true if rect is inside the bounds and corner radii, and if + SkRRect and rect are not empty. + + @param rect area tested for containment + @return true if SkRRect contains rect + + example: https://fiddle.skia.org/c/@RRect_contains + */ + bool contains(const SkRect& rect) const; + + /** Returns true if bounds and radii values are finite and describe a SkRRect + SkRRect::Type that matches getType(). All SkRRect methods construct valid types, + even if the input values are not valid. Invalid SkRRect data can only + be generated by corrupting memory. + + @return true if bounds and radii match type() + + example: https://fiddle.skia.org/c/@RRect_isValid + */ + bool isValid() const; + + static constexpr size_t kSizeInMemory = 12 * sizeof(SkScalar); + + /** Writes SkRRect to buffer. Writes kSizeInMemory bytes, and returns + kSizeInMemory, the number of bytes written. + + @param buffer storage for SkRRect + @return bytes written, kSizeInMemory + + example: https://fiddle.skia.org/c/@RRect_writeToMemory + */ + size_t writeToMemory(void* buffer) const; + + /** Reads SkRRect from buffer, reading kSizeInMemory bytes. + Returns kSizeInMemory, bytes read if length is at least kSizeInMemory. + Otherwise, returns zero. + + @param buffer memory to read from + @param length size of buffer + @return bytes read, or 0 if length is less than kSizeInMemory + + example: https://fiddle.skia.org/c/@RRect_readFromMemory + */ + size_t readFromMemory(const void* buffer, size_t length); + + /** Transforms by SkRRect by matrix, storing result in dst. + Returns true if SkRRect transformed can be represented by another SkRRect. + Returns false if matrix contains transformations that are not axis aligned. + + Asserts in debug builds if SkRRect equals dst. + + @param matrix SkMatrix specifying the transform + @param dst SkRRect to store the result + @return true if transformation succeeded. + + example: https://fiddle.skia.org/c/@RRect_transform + */ + bool transform(const SkMatrix& matrix, SkRRect* dst) const; + + /** Writes text representation of SkRRect to standard output. + Set asHex true to generate exact binary representations + of floating point numbers. + + @param asHex true if SkScalar values are written as hexadecimal + + example: https://fiddle.skia.org/c/@RRect_dump + */ + void dump(bool asHex) const; + SkString dumpToString(bool asHex) const; + + /** Writes text representation of SkRRect to standard output. The representation + may be directly compiled as C++ code. Floating point values are written + with limited precision; it may not be possible to reconstruct original + SkRRect from output. + */ + void dump() const { this->dump(false); } + + /** Writes text representation of SkRRect to standard output. The representation + may be directly compiled as C++ code. Floating point values are written + in hexadecimal to preserve their exact bit pattern. The output reconstructs the + original SkRRect. + */ + void dumpHex() const { this->dump(true); } + +private: + static bool AreRectAndRadiiValid(const SkRect&, const SkVector[4]); + + SkRRect(const SkRect& rect, const SkVector radii[4], int32_t type) + : fRect(rect) + , fRadii{radii[0], radii[1], radii[2], radii[3]} + , fType(type) {} + + /** + * Initializes fRect. If the passed in rect is not finite or empty the rrect will be fully + * initialized and false is returned. Otherwise, just fRect is initialized and true is returned. + */ + bool initializeRect(const SkRect&); + + void computeType(); + bool checkCornerContainment(SkScalar x, SkScalar y) const; + // Returns true if the radii had to be scaled to fit rect + bool scaleRadii(); + + SkRect fRect = SkRect::MakeEmpty(); + // Radii order is UL, UR, LR, LL. Use Corner enum to index into fRadii[] + SkVector fRadii[4] = {{0, 0}, {0, 0}, {0,0}, {0,0}}; + // use an explicitly sized type so we're sure the class is dense (no uninitialized bytes) + int32_t fType = kEmpty_Type; + // TODO: add padding so we can use memcpy for flattening and not copy uninitialized data + + // to access fRadii directly + friend class SkPath; + friend class SkRRectPriv; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRSXform.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRSXform.h new file mode 100644 index 00000000000..de6bafd3581 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRSXform.h @@ -0,0 +1,71 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkRSXform_DEFINED +#define SkRSXform_DEFINED + +#include "include/core/SkPoint.h" +#include "include/core/SkScalar.h" +#include "include/core/SkSize.h" +#include "include/private/base/SkAPI.h" + +/** + * A compressed form of a rotation+scale matrix. + * + * [ fSCos -fSSin fTx ] + * [ fSSin fSCos fTy ] + * [ 0 0 1 ] + */ +struct SK_API SkRSXform { + static SkRSXform Make(SkScalar scos, SkScalar ssin, SkScalar tx, SkScalar ty) { + SkRSXform xform = { scos, ssin, tx, ty }; + return xform; + } + + /* + * Initialize a new xform based on the scale, rotation (in radians), final tx,ty location + * and anchor-point ax,ay within the src quad. + * + * Note: the anchor point is not normalized (e.g. 0...1) but is in pixels of the src image. + */ + static SkRSXform MakeFromRadians(SkScalar scale, SkScalar radians, SkScalar tx, SkScalar ty, + SkScalar ax, SkScalar ay) { + const SkScalar s = SkScalarSin(radians) * scale; + const SkScalar c = SkScalarCos(radians) * scale; + return Make(c, s, tx + -c * ax + s * ay, ty + -s * ax - c * ay); + } + + SkScalar fSCos; + SkScalar fSSin; + SkScalar fTx; + SkScalar fTy; + + bool rectStaysRect() const { + return 0 == fSCos || 0 == fSSin; + } + + void setIdentity() { + fSCos = 1; + fSSin = fTx = fTy = 0; + } + + void set(SkScalar scos, SkScalar ssin, SkScalar tx, SkScalar ty) { + fSCos = scos; + fSSin = ssin; + fTx = tx; + fTy = ty; + } + + void toQuad(SkScalar width, SkScalar height, SkPoint quad[4]) const; + void toQuad(const SkSize& size, SkPoint quad[4]) const { + this->toQuad(size.width(), size.height(), quad); + } + void toTriStrip(SkScalar width, SkScalar height, SkPoint strip[4]) const; +}; + +#endif + diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRasterHandleAllocator.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRasterHandleAllocator.h new file mode 100644 index 00000000000..6fe121a6ded --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRasterHandleAllocator.h @@ -0,0 +1,94 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkRasterHandleAllocator_DEFINED +#define SkRasterHandleAllocator_DEFINED + +#include "include/core/SkImageInfo.h" + +class SkBitmap; +class SkCanvas; +class SkMatrix; +class SkSurfaceProps; + +/** + * If a client wants to control the allocation of raster layers in a canvas, it should subclass + * SkRasterHandleAllocator. This allocator performs two tasks: + * 1. controls how the memory for the pixels is allocated + * 2. associates a "handle" to a private object that can track the matrix/clip of the SkCanvas + * + * This example allocates a canvas, and defers to the allocator to create the base layer. + * + * std::unique_ptr canvas = SkRasterHandleAllocator::MakeCanvas( + * SkImageInfo::Make(...), + * std::make_unique(...), + * nullptr); + * + * If you have already allocated the base layer (and its handle, release-proc etc.) then you + * can pass those in using the last parameter to MakeCanvas(). + * + * Regardless of how the base layer is allocated, each time canvas->saveLayer() is called, + * your allocator's allocHandle() will be called. + */ +class SK_API SkRasterHandleAllocator { +public: + virtual ~SkRasterHandleAllocator() = default; + + // The value that is returned to clients of the canvas that has this allocator installed. + typedef void* Handle; + + struct Rec { + // When the allocation goes out of scope, this proc is called to free everything associated + // with it: the pixels, the "handle", etc. This is passed the pixel address and fReleaseCtx. + void (*fReleaseProc)(void* pixels, void* ctx); + void* fReleaseCtx; // context passed to fReleaseProc + void* fPixels; // pixels for this allocation + size_t fRowBytes; // rowbytes for these pixels + Handle fHandle; // public handle returned by SkCanvas::accessTopRasterHandle() + }; + + /** + * Given a requested info, allocate the corresponding pixels/rowbytes, and whatever handle + * is desired to give clients access to those pixels. The rec also contains a proc and context + * which will be called when this allocation goes out of scope. + * + * e.g. + * when canvas->saveLayer() is called, the allocator will be called to allocate the pixels + * for the layer. When canvas->restore() is called, the fReleaseProc will be called. + */ + virtual bool allocHandle(const SkImageInfo&, Rec*) = 0; + + /** + * Clients access the handle for a given layer by calling SkCanvas::accessTopRasterHandle(). + * To allow the handle to reflect the current matrix/clip in the canvs, updateHandle() is + * is called. The subclass is responsible to update the handle as it sees fit. + */ + virtual void updateHandle(Handle, const SkMatrix&, const SkIRect&) = 0; + + /** + * This creates a canvas which will use the allocator to manage pixel allocations, including + * all calls to saveLayer(). + * + * If rec is non-null, then it will be used as the base-layer of pixels/handle. + * If rec is null, then the allocator will be called for the base-layer as well. + */ + static std::unique_ptr MakeCanvas(std::unique_ptr, + const SkImageInfo&, const Rec* rec = nullptr, + const SkSurfaceProps* props = nullptr); + +protected: + SkRasterHandleAllocator() = default; + SkRasterHandleAllocator(const SkRasterHandleAllocator&) = delete; + SkRasterHandleAllocator& operator=(const SkRasterHandleAllocator&) = delete; + +private: + friend class SkBitmapDevice; + + Handle allocBitmap(const SkImageInfo&, SkBitmap*); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRect.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRect.h new file mode 100644 index 00000000000..65c053a5e16 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRect.h @@ -0,0 +1,1374 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkRect_DEFINED +#define SkRect_DEFINED + +#include "include/core/SkPoint.h" +#include "include/core/SkSize.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkFloatingPoint.h" +#include "include/private/base/SkSafe32.h" +#include "include/private/base/SkTFitsIn.h" + +#include +#include +#include +#include + +struct SkRect; + +/** \struct SkIRect + SkIRect holds four 32-bit integer coordinates describing the upper and + lower bounds of a rectangle. SkIRect may be created from outer bounds or + from position, width, and height. SkIRect describes an area; if its right + is less than or equal to its left, or if its bottom is less than or equal to + its top, it is considered empty. +*/ +struct SK_API SkIRect { + int32_t fLeft = 0; //!< smaller x-axis bounds + int32_t fTop = 0; //!< smaller y-axis bounds + int32_t fRight = 0; //!< larger x-axis bounds + int32_t fBottom = 0; //!< larger y-axis bounds + + /** Returns constructed SkIRect set to (0, 0, 0, 0). + Many other rectangles are empty; if left is equal to or greater than right, + or if top is equal to or greater than bottom. Setting all members to zero + is a convenience, but does not designate a special empty rectangle. + + @return bounds (0, 0, 0, 0) + */ + [[nodiscard]] static constexpr SkIRect MakeEmpty() { + return SkIRect{0, 0, 0, 0}; + } + + /** Returns constructed SkIRect set to (0, 0, w, h). Does not validate input; w or h + may be negative. + + @param w width of constructed SkIRect + @param h height of constructed SkIRect + @return bounds (0, 0, w, h) + */ + [[nodiscard]] static constexpr SkIRect MakeWH(int32_t w, int32_t h) { + return SkIRect{0, 0, w, h}; + } + + /** Returns constructed SkIRect set to (0, 0, size.width(), size.height()). + Does not validate input; size.width() or size.height() may be negative. + + @param size values for SkIRect width and height + @return bounds (0, 0, size.width(), size.height()) + */ + [[nodiscard]] static constexpr SkIRect MakeSize(const SkISize& size) { + return SkIRect{0, 0, size.fWidth, size.fHeight}; + } + + /** Returns constructed SkIRect set to (pt.x(), pt.y(), pt.x() + size.width(), + pt.y() + size.height()). Does not validate input; size.width() or size.height() may be + negative. + + @param pt values for SkIRect fLeft and fTop + @param size values for SkIRect width and height + @return bounds at pt with width and height of size + */ + [[nodiscard]] static constexpr SkIRect MakePtSize(SkIPoint pt, SkISize size) { + return MakeXYWH(pt.x(), pt.y(), size.width(), size.height()); + } + + /** Returns constructed SkIRect set to (l, t, r, b). Does not sort input; SkIRect may + result in fLeft greater than fRight, or fTop greater than fBottom. + + @param l integer stored in fLeft + @param t integer stored in fTop + @param r integer stored in fRight + @param b integer stored in fBottom + @return bounds (l, t, r, b) + */ + [[nodiscard]] static constexpr SkIRect MakeLTRB(int32_t l, int32_t t, int32_t r, int32_t b) { + return SkIRect{l, t, r, b}; + } + + /** Returns constructed SkIRect set to: (x, y, x + w, y + h). + Does not validate input; w or h may be negative. + + @param x stored in fLeft + @param y stored in fTop + @param w added to x and stored in fRight + @param h added to y and stored in fBottom + @return bounds at (x, y) with width w and height h + */ + [[nodiscard]] static constexpr SkIRect MakeXYWH(int32_t x, int32_t y, int32_t w, int32_t h) { + return { x, y, Sk32_sat_add(x, w), Sk32_sat_add(y, h) }; + } + + /** Returns left edge of SkIRect, if sorted. + Call sort() to reverse fLeft and fRight if needed. + + @return fLeft + */ + constexpr int32_t left() const { return fLeft; } + + /** Returns top edge of SkIRect, if sorted. Call isEmpty() to see if SkIRect may be invalid, + and sort() to reverse fTop and fBottom if needed. + + @return fTop + */ + constexpr int32_t top() const { return fTop; } + + /** Returns right edge of SkIRect, if sorted. + Call sort() to reverse fLeft and fRight if needed. + + @return fRight + */ + constexpr int32_t right() const { return fRight; } + + /** Returns bottom edge of SkIRect, if sorted. Call isEmpty() to see if SkIRect may be invalid, + and sort() to reverse fTop and fBottom if needed. + + @return fBottom + */ + constexpr int32_t bottom() const { return fBottom; } + + /** Returns left edge of SkIRect, if sorted. Call isEmpty() to see if SkIRect may be invalid, + and sort() to reverse fLeft and fRight if needed. + + @return fLeft + */ + constexpr int32_t x() const { return fLeft; } + + /** Returns top edge of SkIRect, if sorted. Call isEmpty() to see if SkIRect may be invalid, + and sort() to reverse fTop and fBottom if needed. + + @return fTop + */ + constexpr int32_t y() const { return fTop; } + + // Experimental + constexpr SkIPoint topLeft() const { return {fLeft, fTop}; } + + /** Returns span on the x-axis. This does not check if SkIRect is sorted, or if + result fits in 32-bit signed integer; result may be negative. + + @return fRight minus fLeft + */ + constexpr int32_t width() const { return Sk32_can_overflow_sub(fRight, fLeft); } + + /** Returns span on the y-axis. This does not check if SkIRect is sorted, or if + result fits in 32-bit signed integer; result may be negative. + + @return fBottom minus fTop + */ + constexpr int32_t height() const { return Sk32_can_overflow_sub(fBottom, fTop); } + + /** Returns spans on the x-axis and y-axis. This does not check if SkIRect is sorted, + or if result fits in 32-bit signed integer; result may be negative. + + @return SkISize (width, height) + */ + constexpr SkISize size() const { return SkISize::Make(this->width(), this->height()); } + + /** Returns span on the x-axis. This does not check if SkIRect is sorted, so the + result may be negative. This is safer than calling width() since width() might + overflow in its calculation. + + @return fRight minus fLeft cast to int64_t + */ + constexpr int64_t width64() const { return (int64_t)fRight - (int64_t)fLeft; } + + /** Returns span on the y-axis. This does not check if SkIRect is sorted, so the + result may be negative. This is safer than calling height() since height() might + overflow in its calculation. + + @return fBottom minus fTop cast to int64_t + */ + constexpr int64_t height64() const { return (int64_t)fBottom - (int64_t)fTop; } + + /** Returns true if fLeft is equal to or greater than fRight, or if fTop is equal + to or greater than fBottom. Call sort() to reverse rectangles with negative + width64() or height64(). + + @return true if width64() or height64() are zero or negative + */ + bool isEmpty64() const { return fRight <= fLeft || fBottom <= fTop; } + + /** Returns true if width() or height() are zero or negative. + + @return true if width() or height() are zero or negative + */ + bool isEmpty() const { + int64_t w = this->width64(); + int64_t h = this->height64(); + if (w <= 0 || h <= 0) { + return true; + } + // Return true if either exceeds int32_t + return !SkTFitsIn(w | h); + } + + /** Returns true if all members in a: fLeft, fTop, fRight, and fBottom; are + identical to corresponding members in b. + + @param a SkIRect to compare + @param b SkIRect to compare + @return true if members are equal + */ + friend bool operator==(const SkIRect& a, const SkIRect& b) { + return a.fLeft == b.fLeft && a.fTop == b.fTop && + a.fRight == b.fRight && a.fBottom == b.fBottom; + } + + /** Returns true if any member in a: fLeft, fTop, fRight, and fBottom; is not + identical to the corresponding member in b. + + @param a SkIRect to compare + @param b SkIRect to compare + @return true if members are not equal + */ + friend bool operator!=(const SkIRect& a, const SkIRect& b) { + return a.fLeft != b.fLeft || a.fTop != b.fTop || + a.fRight != b.fRight || a.fBottom != b.fBottom; + } + + /** Sets SkIRect to (0, 0, 0, 0). + + Many other rectangles are empty; if left is equal to or greater than right, + or if top is equal to or greater than bottom. Setting all members to zero + is a convenience, but does not designate a special empty rectangle. + */ + void setEmpty() { memset(this, 0, sizeof(*this)); } + + /** Sets SkIRect to (left, top, right, bottom). + left and right are not sorted; left is not necessarily less than right. + top and bottom are not sorted; top is not necessarily less than bottom. + + @param left stored in fLeft + @param top stored in fTop + @param right stored in fRight + @param bottom stored in fBottom + */ + void setLTRB(int32_t left, int32_t top, int32_t right, int32_t bottom) { + fLeft = left; + fTop = top; + fRight = right; + fBottom = bottom; + } + + /** Sets SkIRect to: (x, y, x + width, y + height). + Does not validate input; width or height may be negative. + + @param x stored in fLeft + @param y stored in fTop + @param width added to x and stored in fRight + @param height added to y and stored in fBottom + */ + void setXYWH(int32_t x, int32_t y, int32_t width, int32_t height) { + fLeft = x; + fTop = y; + fRight = Sk32_sat_add(x, width); + fBottom = Sk32_sat_add(y, height); + } + + void setWH(int32_t width, int32_t height) { + fLeft = 0; + fTop = 0; + fRight = width; + fBottom = height; + } + + void setSize(SkISize size) { + fLeft = 0; + fTop = 0; + fRight = size.width(); + fBottom = size.height(); + } + + /** Returns SkIRect offset by (dx, dy). + + If dx is negative, SkIRect returned is moved to the left. + If dx is positive, SkIRect returned is moved to the right. + If dy is negative, SkIRect returned is moved upward. + If dy is positive, SkIRect returned is moved downward. + + @param dx offset added to fLeft and fRight + @param dy offset added to fTop and fBottom + @return SkIRect offset by dx and dy, with original width and height + */ + constexpr SkIRect makeOffset(int32_t dx, int32_t dy) const { + return { + Sk32_sat_add(fLeft, dx), Sk32_sat_add(fTop, dy), + Sk32_sat_add(fRight, dx), Sk32_sat_add(fBottom, dy), + }; + } + + /** Returns SkIRect offset by (offset.x(), offset.y()). + + If offset.x() is negative, SkIRect returned is moved to the left. + If offset.x() is positive, SkIRect returned is moved to the right. + If offset.y() is negative, SkIRect returned is moved upward. + If offset.y() is positive, SkIRect returned is moved downward. + + @param offset translation vector + @return SkIRect translated by offset, with original width and height + */ + constexpr SkIRect makeOffset(SkIVector offset) const { + return this->makeOffset(offset.x(), offset.y()); + } + + /** Returns SkIRect, inset by (dx, dy). + + If dx is negative, SkIRect returned is wider. + If dx is positive, SkIRect returned is narrower. + If dy is negative, SkIRect returned is taller. + If dy is positive, SkIRect returned is shorter. + + @param dx offset added to fLeft and subtracted from fRight + @param dy offset added to fTop and subtracted from fBottom + @return SkIRect inset symmetrically left and right, top and bottom + */ + SkIRect makeInset(int32_t dx, int32_t dy) const { + return { + Sk32_sat_add(fLeft, dx), Sk32_sat_add(fTop, dy), + Sk32_sat_sub(fRight, dx), Sk32_sat_sub(fBottom, dy), + }; + } + + /** Returns SkIRect, outset by (dx, dy). + + If dx is negative, SkIRect returned is narrower. + If dx is positive, SkIRect returned is wider. + If dy is negative, SkIRect returned is shorter. + If dy is positive, SkIRect returned is taller. + + @param dx offset subtracted to fLeft and added from fRight + @param dy offset subtracted to fTop and added from fBottom + @return SkIRect outset symmetrically left and right, top and bottom + */ + SkIRect makeOutset(int32_t dx, int32_t dy) const { + return { + Sk32_sat_sub(fLeft, dx), Sk32_sat_sub(fTop, dy), + Sk32_sat_add(fRight, dx), Sk32_sat_add(fBottom, dy), + }; + } + + /** Offsets SkIRect by adding dx to fLeft, fRight; and by adding dy to fTop, fBottom. + + If dx is negative, moves SkIRect returned to the left. + If dx is positive, moves SkIRect returned to the right. + If dy is negative, moves SkIRect returned upward. + If dy is positive, moves SkIRect returned downward. + + @param dx offset added to fLeft and fRight + @param dy offset added to fTop and fBottom + */ + void offset(int32_t dx, int32_t dy) { + fLeft = Sk32_sat_add(fLeft, dx); + fTop = Sk32_sat_add(fTop, dy); + fRight = Sk32_sat_add(fRight, dx); + fBottom = Sk32_sat_add(fBottom, dy); + } + + /** Offsets SkIRect by adding delta.fX to fLeft, fRight; and by adding delta.fY to + fTop, fBottom. + + If delta.fX is negative, moves SkIRect returned to the left. + If delta.fX is positive, moves SkIRect returned to the right. + If delta.fY is negative, moves SkIRect returned upward. + If delta.fY is positive, moves SkIRect returned downward. + + @param delta offset added to SkIRect + */ + void offset(const SkIPoint& delta) { + this->offset(delta.fX, delta.fY); + } + + /** Offsets SkIRect so that fLeft equals newX, and fTop equals newY. width and height + are unchanged. + + @param newX stored in fLeft, preserving width() + @param newY stored in fTop, preserving height() + */ + void offsetTo(int32_t newX, int32_t newY) { + fRight = Sk64_pin_to_s32((int64_t)fRight + newX - fLeft); + fBottom = Sk64_pin_to_s32((int64_t)fBottom + newY - fTop); + fLeft = newX; + fTop = newY; + } + + /** Insets SkIRect by (dx,dy). + + If dx is positive, makes SkIRect narrower. + If dx is negative, makes SkIRect wider. + If dy is positive, makes SkIRect shorter. + If dy is negative, makes SkIRect taller. + + @param dx offset added to fLeft and subtracted from fRight + @param dy offset added to fTop and subtracted from fBottom + */ + void inset(int32_t dx, int32_t dy) { + fLeft = Sk32_sat_add(fLeft, dx); + fTop = Sk32_sat_add(fTop, dy); + fRight = Sk32_sat_sub(fRight, dx); + fBottom = Sk32_sat_sub(fBottom, dy); + } + + /** Outsets SkIRect by (dx, dy). + + If dx is positive, makes SkIRect wider. + If dx is negative, makes SkIRect narrower. + If dy is positive, makes SkIRect taller. + If dy is negative, makes SkIRect shorter. + + @param dx subtracted to fLeft and added from fRight + @param dy subtracted to fTop and added from fBottom + */ + void outset(int32_t dx, int32_t dy) { this->inset(-dx, -dy); } + + /** Adjusts SkIRect by adding dL to fLeft, dT to fTop, dR to fRight, and dB to fBottom. + + If dL is positive, narrows SkIRect on the left. If negative, widens it on the left. + If dT is positive, shrinks SkIRect on the top. If negative, lengthens it on the top. + If dR is positive, narrows SkIRect on the right. If negative, widens it on the right. + If dB is positive, shrinks SkIRect on the bottom. If negative, lengthens it on the bottom. + + The resulting SkIRect is not checked for validity. Thus, if the resulting SkIRect left is + greater than right, the SkIRect will be considered empty. Call sort() after this call + if that is not the desired behavior. + + @param dL offset added to fLeft + @param dT offset added to fTop + @param dR offset added to fRight + @param dB offset added to fBottom + */ + void adjust(int32_t dL, int32_t dT, int32_t dR, int32_t dB) { + fLeft = Sk32_sat_add(fLeft, dL); + fTop = Sk32_sat_add(fTop, dT); + fRight = Sk32_sat_add(fRight, dR); + fBottom = Sk32_sat_add(fBottom, dB); + } + + /** Returns true if: fLeft <= x < fRight && fTop <= y < fBottom. + Returns false if SkIRect is empty. + + Considers input to describe constructed SkIRect: (x, y, x + 1, y + 1) and + returns true if constructed area is completely enclosed by SkIRect area. + + @param x test SkIPoint x-coordinate + @param y test SkIPoint y-coordinate + @return true if (x, y) is inside SkIRect + */ + bool contains(int32_t x, int32_t y) const { + return x >= fLeft && x < fRight && y >= fTop && y < fBottom; + } + + /** Returns true if SkIRect contains r. + Returns false if SkIRect is empty or r is empty. + + SkIRect contains r when SkIRect area completely includes r area. + + @param r SkIRect contained + @return true if all sides of SkIRect are outside r + */ + bool contains(const SkIRect& r) const { + return !r.isEmpty() && !this->isEmpty() && // check for empties + fLeft <= r.fLeft && fTop <= r.fTop && + fRight >= r.fRight && fBottom >= r.fBottom; + } + + /** Returns true if SkIRect contains r. + Returns false if SkIRect is empty or r is empty. + + SkIRect contains r when SkIRect area completely includes r area. + + @param r SkRect contained + @return true if all sides of SkIRect are outside r + */ + inline bool contains(const SkRect& r) const; + + /** Returns true if SkIRect contains construction. + Asserts if SkIRect is empty or construction is empty, and if SK_DEBUG is defined. + + Return is undefined if SkIRect is empty or construction is empty. + + @param r SkIRect contained + @return true if all sides of SkIRect are outside r + */ + bool containsNoEmptyCheck(const SkIRect& r) const { + SkASSERT(fLeft < fRight && fTop < fBottom); + SkASSERT(r.fLeft < r.fRight && r.fTop < r.fBottom); + return fLeft <= r.fLeft && fTop <= r.fTop && fRight >= r.fRight && fBottom >= r.fBottom; + } + + /** Returns true if SkIRect intersects r, and sets SkIRect to intersection. + Returns false if SkIRect does not intersect r, and leaves SkIRect unchanged. + + Returns false if either r or SkIRect is empty, leaving SkIRect unchanged. + + @param r limit of result + @return true if r and SkIRect have area in common + */ + bool intersect(const SkIRect& r) { + return this->intersect(*this, r); + } + + /** Returns true if a intersects b, and sets SkIRect to intersection. + Returns false if a does not intersect b, and leaves SkIRect unchanged. + + Returns false if either a or b is empty, leaving SkIRect unchanged. + + @param a SkIRect to intersect + @param b SkIRect to intersect + @return true if a and b have area in common + */ + [[nodiscard]] bool intersect(const SkIRect& a, const SkIRect& b); + + /** Returns true if a intersects b. + Returns false if either a or b is empty, or do not intersect. + + @param a SkIRect to intersect + @param b SkIRect to intersect + @return true if a and b have area in common + */ + static bool Intersects(const SkIRect& a, const SkIRect& b) { + return SkIRect{}.intersect(a, b); + } + + /** Sets SkIRect to the union of itself and r. + + Has no effect if r is empty. Otherwise, if SkIRect is empty, sets SkIRect to r. + + @param r expansion SkIRect + + example: https://fiddle.skia.org/c/@IRect_join_2 + */ + void join(const SkIRect& r); + + /** Swaps fLeft and fRight if fLeft is greater than fRight; and swaps + fTop and fBottom if fTop is greater than fBottom. Result may be empty, + and width() and height() will be zero or positive. + */ + void sort() { + using std::swap; + if (fLeft > fRight) { + swap(fLeft, fRight); + } + if (fTop > fBottom) { + swap(fTop, fBottom); + } + } + + /** Returns SkIRect with fLeft and fRight swapped if fLeft is greater than fRight; and + with fTop and fBottom swapped if fTop is greater than fBottom. Result may be empty; + and width() and height() will be zero or positive. + + @return sorted SkIRect + */ + SkIRect makeSorted() const { + return MakeLTRB(std::min(fLeft, fRight), std::min(fTop, fBottom), + std::max(fLeft, fRight), std::max(fTop, fBottom)); + } +}; + +/** \struct SkRect + SkRect holds four float coordinates describing the upper and + lower bounds of a rectangle. SkRect may be created from outer bounds or + from position, width, and height. SkRect describes an area; if its right + is less than or equal to its left, or if its bottom is less than or equal to + its top, it is considered empty. +*/ +struct SK_API SkRect { + float fLeft = 0; //!< smaller x-axis bounds + float fTop = 0; //!< smaller y-axis bounds + float fRight = 0; //!< larger x-axis bounds + float fBottom = 0; //!< larger y-axis bounds + + /** Returns constructed SkRect set to (0, 0, 0, 0). + Many other rectangles are empty; if left is equal to or greater than right, + or if top is equal to or greater than bottom. Setting all members to zero + is a convenience, but does not designate a special empty rectangle. + + @return bounds (0, 0, 0, 0) + */ + [[nodiscard]] static constexpr SkRect MakeEmpty() { + return SkRect{0, 0, 0, 0}; + } + + /** Returns constructed SkRect set to float values (0, 0, w, h). Does not + validate input; w or h may be negative. + + Passing integer values may generate a compiler warning since SkRect cannot + represent 32-bit integers exactly. Use SkIRect for an exact integer rectangle. + + @param w float width of constructed SkRect + @param h float height of constructed SkRect + @return bounds (0, 0, w, h) + */ + [[nodiscard]] static constexpr SkRect MakeWH(float w, float h) { + return SkRect{0, 0, w, h}; + } + + /** Returns constructed SkRect set to integer values (0, 0, w, h). Does not validate + input; w or h may be negative. + + Use to avoid a compiler warning that input may lose precision when stored. + Use SkIRect for an exact integer rectangle. + + @param w integer width of constructed SkRect + @param h integer height of constructed SkRect + @return bounds (0, 0, w, h) + */ + [[nodiscard]] static SkRect MakeIWH(int w, int h) { + return {0, 0, static_cast(w), static_cast(h)}; + } + + /** Returns constructed SkRect set to (0, 0, size.width(), size.height()). Does not + validate input; size.width() or size.height() may be negative. + + @param size float values for SkRect width and height + @return bounds (0, 0, size.width(), size.height()) + */ + [[nodiscard]] static constexpr SkRect MakeSize(const SkSize& size) { + return SkRect{0, 0, size.fWidth, size.fHeight}; + } + + /** Returns constructed SkRect set to (l, t, r, b). Does not sort input; SkRect may + result in fLeft greater than fRight, or fTop greater than fBottom. + + @param l float stored in fLeft + @param t float stored in fTop + @param r float stored in fRight + @param b float stored in fBottom + @return bounds (l, t, r, b) + */ + [[nodiscard]] static constexpr SkRect MakeLTRB(float l, float t, float r, float b) { + return SkRect {l, t, r, b}; + } + + /** Returns constructed SkRect set to (x, y, x + w, y + h). + Does not validate input; w or h may be negative. + + @param x stored in fLeft + @param y stored in fTop + @param w added to x and stored in fRight + @param h added to y and stored in fBottom + @return bounds at (x, y) with width w and height h + */ + [[nodiscard]] static constexpr SkRect MakeXYWH(float x, float y, float w, float h) { + return SkRect {x, y, x + w, y + h}; + } + + /** Returns constructed SkIRect set to (0, 0, size.width(), size.height()). + Does not validate input; size.width() or size.height() may be negative. + + @param size integer values for SkRect width and height + @return bounds (0, 0, size.width(), size.height()) + */ + static SkRect Make(const SkISize& size) { + return MakeIWH(size.width(), size.height()); + } + + /** Returns constructed SkIRect set to irect, promoting integers to float. + Does not validate input; fLeft may be greater than fRight, fTop may be greater + than fBottom. + + @param irect integer unsorted bounds + @return irect members converted to float + */ + [[nodiscard]] static SkRect Make(const SkIRect& irect) { + return { + static_cast(irect.fLeft), static_cast(irect.fTop), + static_cast(irect.fRight), static_cast(irect.fBottom) + }; + } + + /** Returns true if fLeft is equal to or greater than fRight, or if fTop is equal + to or greater than fBottom. Call sort() to reverse rectangles with negative + width() or height(). + + @return true if width() or height() are zero or negative + */ + bool isEmpty() const { + // We write it as the NOT of a non-empty rect, so we will return true if any values + // are NaN. + return !(fLeft < fRight && fTop < fBottom); + } + + /** Returns true if fLeft is equal to or less than fRight, or if fTop is equal + to or less than fBottom. Call sort() to reverse rectangles with negative + width() or height(). + + @return true if width() or height() are zero or positive + */ + bool isSorted() const { return fLeft <= fRight && fTop <= fBottom; } + + /** Returns true if all values in the rectangle are finite. + + @return true if no member is infinite or NaN + */ + bool isFinite() const { + return SkIsFinite(fLeft, fTop, fRight, fBottom); + } + + /** Returns left edge of SkRect, if sorted. Call isSorted() to see if SkRect is valid. + Call sort() to reverse fLeft and fRight if needed. + + @return fLeft + */ + constexpr float x() const { return fLeft; } + + /** Returns top edge of SkRect, if sorted. Call isEmpty() to see if SkRect may be invalid, + and sort() to reverse fTop and fBottom if needed. + + @return fTop + */ + constexpr float y() const { return fTop; } + + /** Returns left edge of SkRect, if sorted. Call isSorted() to see if SkRect is valid. + Call sort() to reverse fLeft and fRight if needed. + + @return fLeft + */ + constexpr float left() const { return fLeft; } + + /** Returns top edge of SkRect, if sorted. Call isEmpty() to see if SkRect may be invalid, + and sort() to reverse fTop and fBottom if needed. + + @return fTop + */ + constexpr float top() const { return fTop; } + + /** Returns right edge of SkRect, if sorted. Call isSorted() to see if SkRect is valid. + Call sort() to reverse fLeft and fRight if needed. + + @return fRight + */ + constexpr float right() const { return fRight; } + + /** Returns bottom edge of SkRect, if sorted. Call isEmpty() to see if SkRect may be invalid, + and sort() to reverse fTop and fBottom if needed. + + @return fBottom + */ + constexpr float bottom() const { return fBottom; } + + /** Returns span on the x-axis. This does not check if SkRect is sorted, or if + result fits in 32-bit float; result may be negative or infinity. + + @return fRight minus fLeft + */ + constexpr float width() const { return fRight - fLeft; } + + /** Returns span on the y-axis. This does not check if SkRect is sorted, or if + result fits in 32-bit float; result may be negative or infinity. + + @return fBottom minus fTop + */ + constexpr float height() const { return fBottom - fTop; } + + /** Returns average of left edge and right edge. Result does not change if SkRect + is sorted. Result may overflow to infinity if SkRect is far from the origin. + + @return midpoint on x-axis + */ + constexpr float centerX() const { + return sk_float_midpoint(fLeft, fRight); + } + + /** Returns average of top edge and bottom edge. Result does not change if SkRect + is sorted. + + @return midpoint on y-axis + */ + constexpr float centerY() const { + return sk_float_midpoint(fTop, fBottom); + } + + /** Returns the point this->centerX(), this->centerY(). + @return rectangle center + */ + constexpr SkPoint center() const { return {this->centerX(), this->centerY()}; } + + /** Returns true if all members in a: fLeft, fTop, fRight, and fBottom; are + equal to the corresponding members in b. + + a and b are not equal if either contain NaN. a and b are equal if members + contain zeroes with different signs. + + @param a SkRect to compare + @param b SkRect to compare + @return true if members are equal + */ + friend bool operator==(const SkRect& a, const SkRect& b) { + return a.fLeft == b.fLeft && + a.fTop == b.fTop && + a.fRight == b.fRight && + a.fBottom == b.fBottom; + } + + /** Returns true if any in a: fLeft, fTop, fRight, and fBottom; does not + equal the corresponding members in b. + + a and b are not equal if either contain NaN. a and b are equal if members + contain zeroes with different signs. + + @param a SkRect to compare + @param b SkRect to compare + @return true if members are not equal + */ + friend bool operator!=(const SkRect& a, const SkRect& b) { + return !(a == b); + } + + /** Returns four points in quad that enclose SkRect ordered as: top-left, top-right, + bottom-right, bottom-left. + + TODO: Consider adding parameter to control whether quad is clockwise or counterclockwise. + + @param quad storage for corners of SkRect + + example: https://fiddle.skia.org/c/@Rect_toQuad + */ + void toQuad(SkPoint quad[4]) const; + + /** Sets SkRect to (0, 0, 0, 0). + + Many other rectangles are empty; if left is equal to or greater than right, + or if top is equal to or greater than bottom. Setting all members to zero + is a convenience, but does not designate a special empty rectangle. + */ + void setEmpty() { *this = MakeEmpty(); } + + /** Sets SkRect to src, promoting src members from integer to float. + Very large values in src may lose precision. + + @param src integer SkRect + */ + void set(const SkIRect& src) { + fLeft = src.fLeft; + fTop = src.fTop; + fRight = src.fRight; + fBottom = src.fBottom; + } + + /** Sets SkRect to (left, top, right, bottom). + left and right are not sorted; left is not necessarily less than right. + top and bottom are not sorted; top is not necessarily less than bottom. + + @param left stored in fLeft + @param top stored in fTop + @param right stored in fRight + @param bottom stored in fBottom + */ + void setLTRB(float left, float top, float right, float bottom) { + fLeft = left; + fTop = top; + fRight = right; + fBottom = bottom; + } + + /** Sets to bounds of SkPoint array with count entries. If count is zero or smaller, + or if SkPoint array contains an infinity or NaN, sets to (0, 0, 0, 0). + + Result is either empty or sorted: fLeft is less than or equal to fRight, and + fTop is less than or equal to fBottom. + + @param pts SkPoint array + @param count entries in array + */ + void setBounds(const SkPoint pts[], int count) { + (void)this->setBoundsCheck(pts, count); + } + + /** Sets to bounds of SkPoint array with count entries. Returns false if count is + zero or smaller, or if SkPoint array contains an infinity or NaN; in these cases + sets SkRect to (0, 0, 0, 0). + + Result is either empty or sorted: fLeft is less than or equal to fRight, and + fTop is less than or equal to fBottom. + + @param pts SkPoint array + @param count entries in array + @return true if all SkPoint values are finite + + example: https://fiddle.skia.org/c/@Rect_setBoundsCheck + */ + bool setBoundsCheck(const SkPoint pts[], int count); + + /** Sets to bounds of SkPoint pts array with count entries. If any SkPoint in pts + contains infinity or NaN, all SkRect dimensions are set to NaN. + + @param pts SkPoint array + @param count entries in array + + example: https://fiddle.skia.org/c/@Rect_setBoundsNoCheck + */ + void setBoundsNoCheck(const SkPoint pts[], int count); + + /** Sets bounds to the smallest SkRect enclosing SkPoint p0 and p1. The result is + sorted and may be empty. Does not check to see if values are finite. + + @param p0 corner to include + @param p1 corner to include + */ + void set(const SkPoint& p0, const SkPoint& p1) { + fLeft = std::min(p0.fX, p1.fX); + fRight = std::max(p0.fX, p1.fX); + fTop = std::min(p0.fY, p1.fY); + fBottom = std::max(p0.fY, p1.fY); + } + + /** Sets SkRect to (x, y, x + width, y + height). + Does not validate input; width or height may be negative. + + @param x stored in fLeft + @param y stored in fTop + @param width added to x and stored in fRight + @param height added to y and stored in fBottom + */ + void setXYWH(float x, float y, float width, float height) { + fLeft = x; + fTop = y; + fRight = x + width; + fBottom = y + height; + } + + /** Sets SkRect to (0, 0, width, height). Does not validate input; + width or height may be negative. + + @param width stored in fRight + @param height stored in fBottom + */ + void setWH(float width, float height) { + fLeft = 0; + fTop = 0; + fRight = width; + fBottom = height; + } + void setIWH(int32_t width, int32_t height) { + this->setWH(width, height); + } + + /** Returns SkRect offset by (dx, dy). + + If dx is negative, SkRect returned is moved to the left. + If dx is positive, SkRect returned is moved to the right. + If dy is negative, SkRect returned is moved upward. + If dy is positive, SkRect returned is moved downward. + + @param dx added to fLeft and fRight + @param dy added to fTop and fBottom + @return SkRect offset on axes, with original width and height + */ + constexpr SkRect makeOffset(float dx, float dy) const { + return MakeLTRB(fLeft + dx, fTop + dy, fRight + dx, fBottom + dy); + } + + /** Returns SkRect offset by v. + + @param v added to rect + @return SkRect offset on axes, with original width and height + */ + constexpr SkRect makeOffset(SkVector v) const { return this->makeOffset(v.x(), v.y()); } + + /** Returns SkRect, inset by (dx, dy). + + If dx is negative, SkRect returned is wider. + If dx is positive, SkRect returned is narrower. + If dy is negative, SkRect returned is taller. + If dy is positive, SkRect returned is shorter. + + @param dx added to fLeft and subtracted from fRight + @param dy added to fTop and subtracted from fBottom + @return SkRect inset symmetrically left and right, top and bottom + */ + SkRect makeInset(float dx, float dy) const { + return MakeLTRB(fLeft + dx, fTop + dy, fRight - dx, fBottom - dy); + } + + /** Returns SkRect, outset by (dx, dy). + + If dx is negative, SkRect returned is narrower. + If dx is positive, SkRect returned is wider. + If dy is negative, SkRect returned is shorter. + If dy is positive, SkRect returned is taller. + + @param dx subtracted to fLeft and added from fRight + @param dy subtracted to fTop and added from fBottom + @return SkRect outset symmetrically left and right, top and bottom + */ + SkRect makeOutset(float dx, float dy) const { + return MakeLTRB(fLeft - dx, fTop - dy, fRight + dx, fBottom + dy); + } + + /** Offsets SkRect by adding dx to fLeft, fRight; and by adding dy to fTop, fBottom. + + If dx is negative, moves SkRect to the left. + If dx is positive, moves SkRect to the right. + If dy is negative, moves SkRect upward. + If dy is positive, moves SkRect downward. + + @param dx offset added to fLeft and fRight + @param dy offset added to fTop and fBottom + */ + void offset(float dx, float dy) { + fLeft += dx; + fTop += dy; + fRight += dx; + fBottom += dy; + } + + /** Offsets SkRect by adding delta.fX to fLeft, fRight; and by adding delta.fY to + fTop, fBottom. + + If delta.fX is negative, moves SkRect to the left. + If delta.fX is positive, moves SkRect to the right. + If delta.fY is negative, moves SkRect upward. + If delta.fY is positive, moves SkRect downward. + + @param delta added to SkRect + */ + void offset(const SkPoint& delta) { + this->offset(delta.fX, delta.fY); + } + + /** Offsets SkRect so that fLeft equals newX, and fTop equals newY. width and height + are unchanged. + + @param newX stored in fLeft, preserving width() + @param newY stored in fTop, preserving height() + */ + void offsetTo(float newX, float newY) { + fRight += newX - fLeft; + fBottom += newY - fTop; + fLeft = newX; + fTop = newY; + } + + /** Insets SkRect by (dx, dy). + + If dx is positive, makes SkRect narrower. + If dx is negative, makes SkRect wider. + If dy is positive, makes SkRect shorter. + If dy is negative, makes SkRect taller. + + @param dx added to fLeft and subtracted from fRight + @param dy added to fTop and subtracted from fBottom + */ + void inset(float dx, float dy) { + fLeft += dx; + fTop += dy; + fRight -= dx; + fBottom -= dy; + } + + /** Outsets SkRect by (dx, dy). + + If dx is positive, makes SkRect wider. + If dx is negative, makes SkRect narrower. + If dy is positive, makes SkRect taller. + If dy is negative, makes SkRect shorter. + + @param dx subtracted to fLeft and added from fRight + @param dy subtracted to fTop and added from fBottom + */ + void outset(float dx, float dy) { this->inset(-dx, -dy); } + + /** Returns true if SkRect intersects r, and sets SkRect to intersection. + Returns false if SkRect does not intersect r, and leaves SkRect unchanged. + + Returns false if either r or SkRect is empty, leaving SkRect unchanged. + + @param r limit of result + @return true if r and SkRect have area in common + + example: https://fiddle.skia.org/c/@Rect_intersect + */ + bool intersect(const SkRect& r); + + /** Returns true if a intersects b, and sets SkRect to intersection. + Returns false if a does not intersect b, and leaves SkRect unchanged. + + Returns false if either a or b is empty, leaving SkRect unchanged. + + @param a SkRect to intersect + @param b SkRect to intersect + @return true if a and b have area in common + */ + [[nodiscard]] bool intersect(const SkRect& a, const SkRect& b); + + +private: + static bool Intersects(float al, float at, float ar, float ab, + float bl, float bt, float br, float bb) { + float L = std::max(al, bl); + float R = std::min(ar, br); + float T = std::max(at, bt); + float B = std::min(ab, bb); + return L < R && T < B; + } + +public: + + /** Returns true if SkRect intersects r. + Returns false if either r or SkRect is empty, or do not intersect. + + @param r SkRect to intersect + @return true if r and SkRect have area in common + */ + bool intersects(const SkRect& r) const { + return Intersects(fLeft, fTop, fRight, fBottom, + r.fLeft, r.fTop, r.fRight, r.fBottom); + } + + /** Returns true if a intersects b. + Returns false if either a or b is empty, or do not intersect. + + @param a SkRect to intersect + @param b SkRect to intersect + @return true if a and b have area in common + */ + static bool Intersects(const SkRect& a, const SkRect& b) { + return Intersects(a.fLeft, a.fTop, a.fRight, a.fBottom, + b.fLeft, b.fTop, b.fRight, b.fBottom); + } + + /** Sets SkRect to the union of itself and r. + + Has no effect if r is empty. Otherwise, if SkRect is empty, sets + SkRect to r. + + @param r expansion SkRect + + example: https://fiddle.skia.org/c/@Rect_join_2 + */ + void join(const SkRect& r); + + /** Sets SkRect to the union of itself and r. + + Asserts if r is empty and SK_DEBUG is defined. + If SkRect is empty, sets SkRect to r. + + May produce incorrect results if r is empty. + + @param r expansion SkRect + */ + void joinNonEmptyArg(const SkRect& r) { + SkASSERT(!r.isEmpty()); + // if we are empty, just assign + if (fLeft >= fRight || fTop >= fBottom) { + *this = r; + } else { + this->joinPossiblyEmptyRect(r); + } + } + + /** Sets SkRect to the union of itself and the construction. + + May produce incorrect results if SkRect or r is empty. + + @param r expansion SkRect + */ + void joinPossiblyEmptyRect(const SkRect& r) { + fLeft = std::min(fLeft, r.left()); + fTop = std::min(fTop, r.top()); + fRight = std::max(fRight, r.right()); + fBottom = std::max(fBottom, r.bottom()); + } + + /** Returns true if: fLeft <= x < fRight && fTop <= y < fBottom. + Returns false if SkRect is empty. + + @param x test SkPoint x-coordinate + @param y test SkPoint y-coordinate + @return true if (x, y) is inside SkRect + */ + bool contains(float x, float y) const { + return x >= fLeft && x < fRight && y >= fTop && y < fBottom; + } + + /** Returns true if SkRect contains r. + Returns false if SkRect is empty or r is empty. + + SkRect contains r when SkRect area completely includes r area. + + @param r SkRect contained + @return true if all sides of SkRect are outside r + */ + bool contains(const SkRect& r) const { + // todo: can we eliminate the this->isEmpty check? + return !r.isEmpty() && !this->isEmpty() && + fLeft <= r.fLeft && fTop <= r.fTop && + fRight >= r.fRight && fBottom >= r.fBottom; + } + + /** Returns true if SkRect contains r. + Returns false if SkRect is empty or r is empty. + + SkRect contains r when SkRect area completely includes r area. + + @param r SkIRect contained + @return true if all sides of SkRect are outside r + */ + bool contains(const SkIRect& r) const { + // todo: can we eliminate the this->isEmpty check? + return !r.isEmpty() && !this->isEmpty() && + fLeft <= r.fLeft && fTop <= r.fTop && + fRight >= r.fRight && fBottom >= r.fBottom; + } + + /** Sets SkIRect by adding 0.5 and discarding the fractional portion of SkRect + members, using (sk_float_round2int(fLeft), sk_float_round2int(fTop), + sk_float_round2int(fRight), sk_float_round2int(fBottom)). + + @param dst storage for SkIRect + */ + void round(SkIRect* dst) const { + SkASSERT(dst); + dst->setLTRB(sk_float_round2int(fLeft), sk_float_round2int(fTop), + sk_float_round2int(fRight), sk_float_round2int(fBottom)); + } + + /** Sets SkIRect by discarding the fractional portion of fLeft and fTop; and rounding + up fRight and fBottom, using + (sk_float_floor2int(fLeft), sk_float_floor2int(fTop), + sk_float_ceil2int(fRight), sk_float_ceil2int(fBottom)). + + @param dst storage for SkIRect + */ + void roundOut(SkIRect* dst) const { + SkASSERT(dst); + dst->setLTRB(sk_float_floor2int(fLeft), sk_float_floor2int(fTop), + sk_float_ceil2int(fRight), sk_float_ceil2int(fBottom)); + } + + /** Sets SkRect by discarding the fractional portion of fLeft and fTop; and rounding + up fRight and fBottom, using + (std::floor(fLeft), std::floor(fTop), + std::ceil(fRight), std::ceil(fBottom)). + + @param dst storage for SkRect + */ + void roundOut(SkRect* dst) const { + dst->setLTRB(std::floor(fLeft), std::floor(fTop), + std::ceil(fRight), std::ceil(fBottom)); + } + + /** Sets SkRect by rounding up fLeft and fTop; and discarding the fractional portion + of fRight and fBottom, using + (sk_float_ceil2int(fLeft), sk_float_ceil2int(fTop), + sk_float_floor2int(fRight), sk_float_floor2int(fBottom)). + + @param dst storage for SkIRect + */ + void roundIn(SkIRect* dst) const { + SkASSERT(dst); + dst->setLTRB(sk_float_ceil2int(fLeft), sk_float_ceil2int(fTop), + sk_float_floor2int(fRight), sk_float_floor2int(fBottom)); + } + + /** Returns SkIRect by adding 0.5 and discarding the fractional portion of SkRect + members, using (sk_float_round2int(fLeft), sk_float_round2int(fTop), + sk_float_round2int(fRight), sk_float_round2int(fBottom)). + + @return rounded SkIRect + */ + SkIRect round() const { + SkIRect ir; + this->round(&ir); + return ir; + } + + /** Sets SkIRect by discarding the fractional portion of fLeft and fTop; and rounding + up fRight and fBottom, using + (sk_float_floor2int(fLeft), sk_float_floor2int(fTop), + sk_float_ceil2int(fRight), sk_float_ceil2int(fBottom)). + + @return rounded SkIRect + */ + SkIRect roundOut() const { + SkIRect ir; + this->roundOut(&ir); + return ir; + } + /** Sets SkIRect by rounding up fLeft and fTop; and discarding the fractional portion + of fRight and fBottom, using + (sk_float_ceil2int(fLeft), sk_float_ceil2int(fTop), + sk_float_floor2int(fRight), sk_float_floor2int(fBottom)). + + @return rounded SkIRect + */ + SkIRect roundIn() const { + SkIRect ir; + this->roundIn(&ir); + return ir; + } + + /** Swaps fLeft and fRight if fLeft is greater than fRight; and swaps + fTop and fBottom if fTop is greater than fBottom. Result may be empty; + and width() and height() will be zero or positive. + */ + void sort() { + using std::swap; + if (fLeft > fRight) { + swap(fLeft, fRight); + } + + if (fTop > fBottom) { + swap(fTop, fBottom); + } + } + + /** Returns SkRect with fLeft and fRight swapped if fLeft is greater than fRight; and + with fTop and fBottom swapped if fTop is greater than fBottom. Result may be empty; + and width() and height() will be zero or positive. + + @return sorted SkRect + */ + SkRect makeSorted() const { + return MakeLTRB(std::min(fLeft, fRight), std::min(fTop, fBottom), + std::max(fLeft, fRight), std::max(fTop, fBottom)); + } + + /** Returns pointer to first float in SkRect, to treat it as an array with four + entries. + + @return pointer to fLeft + */ + const float* asScalars() const { return &fLeft; } + + /** Writes text representation of SkRect to standard output. Set asHex to true to + generate exact binary representations of floating point numbers. + + @param asHex true if SkScalar values are written as hexadecimal + + example: https://fiddle.skia.org/c/@Rect_dump + */ + void dump(bool asHex) const; + + /** Writes text representation of SkRect to standard output. The representation may be + directly compiled as C++ code. Floating point values are written + with limited precision; it may not be possible to reconstruct original SkRect + from output. + */ + void dump() const { this->dump(false); } + + /** Writes text representation of SkRect to standard output. The representation may be + directly compiled as C++ code. Floating point values are written + in hexadecimal to preserve their exact bit pattern. The output reconstructs the + original SkRect. + + Use instead of dump() when submitting + */ + void dumpHex() const { this->dump(true); } +}; + +inline bool SkIRect::contains(const SkRect& r) const { + return !r.isEmpty() && !this->isEmpty() && // check for empties + fLeft <= r.fLeft && fTop <= r.fTop && + fRight >= r.fRight && fBottom >= r.fBottom; +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRefCnt.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRefCnt.h new file mode 100644 index 00000000000..34274a461d3 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRefCnt.h @@ -0,0 +1,389 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkRefCnt_DEFINED +#define SkRefCnt_DEFINED + +#include "include/core/SkTypes.h" +#include "include/private/base/SkDebug.h" + +#include +#include +#include +#include +#include +#include + +/** \class SkRefCntBase + + SkRefCntBase is the base class for objects that may be shared by multiple + objects. When an existing owner wants to share a reference, it calls ref(). + When an owner wants to release its reference, it calls unref(). When the + shared object's reference count goes to zero as the result of an unref() + call, its (virtual) destructor is called. It is an error for the + destructor to be called explicitly (or via the object going out of scope on + the stack or calling delete) if getRefCnt() > 1. +*/ +class SK_API SkRefCntBase { +public: + /** Default construct, initializing the reference count to 1. + */ + SkRefCntBase() : fRefCnt(1) {} + + /** Destruct, asserting that the reference count is 1. + */ + virtual ~SkRefCntBase() { + #ifdef SK_DEBUG + SkASSERTF(this->getRefCnt() == 1, "fRefCnt was %d", this->getRefCnt()); + // illegal value, to catch us if we reuse after delete + fRefCnt.store(0, std::memory_order_relaxed); + #endif + } + + /** May return true if the caller is the only owner. + * Ensures that all previous owner's actions are complete. + */ + bool unique() const { + if (1 == fRefCnt.load(std::memory_order_acquire)) { + // The acquire barrier is only really needed if we return true. It + // prevents code conditioned on the result of unique() from running + // until previous owners are all totally done calling unref(). + return true; + } + return false; + } + + /** Increment the reference count. Must be balanced by a call to unref(). + */ + void ref() const { + SkASSERT(this->getRefCnt() > 0); + // No barrier required. + (void)fRefCnt.fetch_add(+1, std::memory_order_relaxed); + } + + /** Decrement the reference count. If the reference count is 1 before the + decrement, then delete the object. Note that if this is the case, then + the object needs to have been allocated via new, and not on the stack. + */ + void unref() const { + SkASSERT(this->getRefCnt() > 0); + // A release here acts in place of all releases we "should" have been doing in ref(). + if (1 == fRefCnt.fetch_add(-1, std::memory_order_acq_rel)) { + // Like unique(), the acquire is only needed on success, to make sure + // code in internal_dispose() doesn't happen before the decrement. + this->internal_dispose(); + } + } + +private: + +#ifdef SK_DEBUG + /** Return the reference count. Use only for debugging. */ + int32_t getRefCnt() const { + return fRefCnt.load(std::memory_order_relaxed); + } +#endif + + /** + * Called when the ref count goes to 0. + */ + virtual void internal_dispose() const { + #ifdef SK_DEBUG + SkASSERT(0 == this->getRefCnt()); + fRefCnt.store(1, std::memory_order_relaxed); + #endif + delete this; + } + + // The following friends are those which override internal_dispose() + // and conditionally call SkRefCnt::internal_dispose(). + friend class SkWeakRefCnt; + + mutable std::atomic fRefCnt; + + SkRefCntBase(SkRefCntBase&&) = delete; + SkRefCntBase(const SkRefCntBase&) = delete; + SkRefCntBase& operator=(SkRefCntBase&&) = delete; + SkRefCntBase& operator=(const SkRefCntBase&) = delete; +}; + +#ifdef SK_REF_CNT_MIXIN_INCLUDE +// It is the responsibility of the following include to define the type SkRefCnt. +// This SkRefCnt should normally derive from SkRefCntBase. +#include SK_REF_CNT_MIXIN_INCLUDE +#else +class SK_API SkRefCnt : public SkRefCntBase { + // "#include SK_REF_CNT_MIXIN_INCLUDE" doesn't work with this build system. + #if defined(SK_BUILD_FOR_GOOGLE3) + public: + void deref() const { this->unref(); } + #endif +}; +#endif + +/////////////////////////////////////////////////////////////////////////////// + +/** Call obj->ref() and return obj. The obj must not be nullptr. + */ +template static inline T* SkRef(T* obj) { + SkASSERT(obj); + obj->ref(); + return obj; +} + +/** Check if the argument is non-null, and if so, call obj->ref() and return obj. + */ +template static inline T* SkSafeRef(T* obj) { + if (obj) { + obj->ref(); + } + return obj; +} + +/** Check if the argument is non-null, and if so, call obj->unref() + */ +template static inline void SkSafeUnref(T* obj) { + if (obj) { + obj->unref(); + } +} + +/////////////////////////////////////////////////////////////////////////////// + +// This is a variant of SkRefCnt that's Not Virtual, so weighs 4 bytes instead of 8 or 16. +// There's only benefit to using this if the deriving class does not otherwise need a vtable. +template +class SkNVRefCnt { +public: + SkNVRefCnt() : fRefCnt(1) {} + ~SkNVRefCnt() { + #ifdef SK_DEBUG + int rc = fRefCnt.load(std::memory_order_relaxed); + SkASSERTF(rc == 1, "NVRefCnt was %d", rc); + #endif + } + + // Implementation is pretty much the same as SkRefCntBase. All required barriers are the same: + // - unique() needs acquire when it returns true, and no barrier if it returns false; + // - ref() doesn't need any barrier; + // - unref() needs a release barrier, and an acquire if it's going to call delete. + + bool unique() const { return 1 == fRefCnt.load(std::memory_order_acquire); } + void ref() const { (void)fRefCnt.fetch_add(+1, std::memory_order_relaxed); } + void unref() const { + if (1 == fRefCnt.fetch_add(-1, std::memory_order_acq_rel)) { + // restore the 1 for our destructor's assert + SkDEBUGCODE(fRefCnt.store(1, std::memory_order_relaxed)); + delete (const Derived*)this; + } + } + void deref() const { this->unref(); } + + // This must be used with caution. It is only valid to call this when 'threadIsolatedTestCnt' + // refs are known to be isolated to the current thread. That is, it is known that there are at + // least 'threadIsolatedTestCnt' refs for which no other thread may make a balancing unref() + // call. Assuming the contract is followed, if this returns false then no other thread has + // ownership of this. If it returns true then another thread *may* have ownership. + bool refCntGreaterThan(int32_t threadIsolatedTestCnt) const { + int cnt = fRefCnt.load(std::memory_order_acquire); + // If this fails then the above contract has been violated. + SkASSERT(cnt >= threadIsolatedTestCnt); + return cnt > threadIsolatedTestCnt; + } + +private: + mutable std::atomic fRefCnt; + + SkNVRefCnt(SkNVRefCnt&&) = delete; + SkNVRefCnt(const SkNVRefCnt&) = delete; + SkNVRefCnt& operator=(SkNVRefCnt&&) = delete; + SkNVRefCnt& operator=(const SkNVRefCnt&) = delete; +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Shared pointer class to wrap classes that support a ref()/unref() interface. + * + * This can be used for classes inheriting from SkRefCnt, but it also works for other + * classes that match the interface, but have different internal choices: e.g. the hosted class + * may have its ref/unref be thread-safe, but that is not assumed/imposed by sk_sp. + * + * Declared with the trivial_abi attribute where supported so that sk_sp and types containing it + * may be considered as trivially relocatable by the compiler so that destroying-move operations + * i.e. move constructor followed by destructor can be optimized to memcpy. + */ +template class SK_TRIVIAL_ABI sk_sp { +public: + using element_type = T; + + constexpr sk_sp() : fPtr(nullptr) {} + constexpr sk_sp(std::nullptr_t) : fPtr(nullptr) {} + + /** + * Shares the underlying object by calling ref(), so that both the argument and the newly + * created sk_sp both have a reference to it. + */ + sk_sp(const sk_sp& that) : fPtr(SkSafeRef(that.get())) {} + template ::value>::type> + sk_sp(const sk_sp& that) : fPtr(SkSafeRef(that.get())) {} + + /** + * Move the underlying object from the argument to the newly created sk_sp. Afterwards only + * the new sk_sp will have a reference to the object, and the argument will point to null. + * No call to ref() or unref() will be made. + */ + sk_sp(sk_sp&& that) : fPtr(that.release()) {} + template ::value>::type> + sk_sp(sk_sp&& that) : fPtr(that.release()) {} + + /** + * Adopt the bare pointer into the newly created sk_sp. + * No call to ref() or unref() will be made. + */ + explicit sk_sp(T* obj) : fPtr(obj) {} + + /** + * Calls unref() on the underlying object pointer. + */ + ~sk_sp() { + SkSafeUnref(fPtr); + SkDEBUGCODE(fPtr = nullptr); + } + + sk_sp& operator=(std::nullptr_t) { this->reset(); return *this; } + + /** + * Shares the underlying object referenced by the argument by calling ref() on it. If this + * sk_sp previously had a reference to an object (i.e. not null) it will call unref() on that + * object. + */ + sk_sp& operator=(const sk_sp& that) { + if (this != &that) { + this->reset(SkSafeRef(that.get())); + } + return *this; + } + template ::value>::type> + sk_sp& operator=(const sk_sp& that) { + this->reset(SkSafeRef(that.get())); + return *this; + } + + /** + * Move the underlying object from the argument to the sk_sp. If the sk_sp previously held + * a reference to another object, unref() will be called on that object. No call to ref() + * will be made. + */ + sk_sp& operator=(sk_sp&& that) { + this->reset(that.release()); + return *this; + } + template ::value>::type> + sk_sp& operator=(sk_sp&& that) { + this->reset(that.release()); + return *this; + } + + T& operator*() const { + SkASSERT(this->get() != nullptr); + return *this->get(); + } + + explicit operator bool() const { return this->get() != nullptr; } + + T* get() const { return fPtr; } + T* operator->() const { return fPtr; } + + /** + * Adopt the new bare pointer, and call unref() on any previously held object (if not null). + * No call to ref() will be made. + */ + void reset(T* ptr = nullptr) { + // Calling fPtr->unref() may call this->~() or this->reset(T*). + // http://wg21.cmeerw.net/lwg/issue998 + // http://wg21.cmeerw.net/lwg/issue2262 + T* oldPtr = fPtr; + fPtr = ptr; + SkSafeUnref(oldPtr); + } + + /** + * Return the bare pointer, and set the internal object pointer to nullptr. + * The caller must assume ownership of the object, and manage its reference count directly. + * No call to unref() will be made. + */ + [[nodiscard]] T* release() { + T* ptr = fPtr; + fPtr = nullptr; + return ptr; + } + + void swap(sk_sp& that) /*noexcept*/ { + using std::swap; + swap(fPtr, that.fPtr); + } + + using sk_is_trivially_relocatable = std::true_type; + +private: + T* fPtr; +}; + +template inline void swap(sk_sp& a, sk_sp& b) /*noexcept*/ { + a.swap(b); +} + +template inline bool operator==(const sk_sp& a, const sk_sp& b) { + return a.get() == b.get(); +} +template inline bool operator==(const sk_sp& a, std::nullptr_t) /*noexcept*/ { + return !a; +} +template inline bool operator==(std::nullptr_t, const sk_sp& b) /*noexcept*/ { + return !b; +} + +template inline bool operator!=(const sk_sp& a, const sk_sp& b) { + return a.get() != b.get(); +} +template inline bool operator!=(const sk_sp& a, std::nullptr_t) /*noexcept*/ { + return static_cast(a); +} +template inline bool operator!=(std::nullptr_t, const sk_sp& b) /*noexcept*/ { + return static_cast(b); +} + +template +auto operator<<(std::basic_ostream& os, const sk_sp& sp) -> decltype(os << sp.get()) { + return os << sp.get(); +} + +template +sk_sp sk_make_sp(Args&&... args) { + return sk_sp(new T(std::forward(args)...)); +} + +/* + * Returns a sk_sp wrapping the provided ptr AND calls ref on it (if not null). + * + * This is different than the semantics of the constructor for sk_sp, which just wraps the ptr, + * effectively "adopting" it. + */ +template sk_sp sk_ref_sp(T* obj) { + return sk_sp(SkSafeRef(obj)); +} + +template sk_sp sk_ref_sp(const T* obj) { + return sk_sp(const_cast(SkSafeRef(obj))); +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRegion.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRegion.h new file mode 100644 index 00000000000..d72cd2ab7df --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkRegion.h @@ -0,0 +1,684 @@ +/* + * Copyright 2005 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkRegion_DEFINED +#define SkRegion_DEFINED + +#include "include/core/SkRect.h" +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkAssert.h" +#include "include/private/base/SkDebug.h" +#include "include/private/base/SkTypeTraits.h" + +#include +#include +#include + +class SkPath; + +/** \class SkRegion + SkRegion describes the set of pixels used to clip SkCanvas. SkRegion is compact, + efficiently storing a single integer rectangle, or a run length encoded array + of rectangles. SkRegion may reduce the current SkCanvas clip, or may be drawn as + one or more integer rectangles. SkRegion iterator returns the scan lines or + rectangles contained by it, optionally intersecting a bounding rectangle. +*/ +class SK_API SkRegion { + typedef int32_t RunType; +public: + + /** Constructs an empty SkRegion. SkRegion is set to empty bounds + at (0, 0) with zero width and height. + + @return empty SkRegion + + example: https://fiddle.skia.org/c/@Region_empty_constructor + */ + SkRegion(); + + /** Constructs a copy of an existing region. + Copy constructor makes two regions identical by value. Internally, region and + the returned result share pointer values. The underlying SkRect array is + copied when modified. + + Creating a SkRegion copy is very efficient and never allocates memory. + SkRegion are always copied by value from the interface; the underlying shared + pointers are not exposed. + + @param region SkRegion to copy by value + @return copy of SkRegion + + example: https://fiddle.skia.org/c/@Region_copy_const_SkRegion + */ + SkRegion(const SkRegion& region); + + /** Constructs a rectangular SkRegion matching the bounds of rect. + + @param rect bounds of constructed SkRegion + @return rectangular SkRegion + + example: https://fiddle.skia.org/c/@Region_copy_const_SkIRect + */ + explicit SkRegion(const SkIRect& rect); + + /** Releases ownership of any shared data and deletes data if SkRegion is sole owner. + + example: https://fiddle.skia.org/c/@Region_destructor + */ + ~SkRegion(); + + /** Constructs a copy of an existing region. + Makes two regions identical by value. Internally, region and + the returned result share pointer values. The underlying SkRect array is + copied when modified. + + Creating a SkRegion copy is very efficient and never allocates memory. + SkRegion are always copied by value from the interface; the underlying shared + pointers are not exposed. + + @param region SkRegion to copy by value + @return SkRegion to copy by value + + example: https://fiddle.skia.org/c/@Region_copy_operator + */ + SkRegion& operator=(const SkRegion& region); + + /** Compares SkRegion and other; returns true if they enclose exactly + the same area. + + @param other SkRegion to compare + @return true if SkRegion pair are equivalent + + example: https://fiddle.skia.org/c/@Region_equal1_operator + */ + bool operator==(const SkRegion& other) const; + + /** Compares SkRegion and other; returns true if they do not enclose the same area. + + @param other SkRegion to compare + @return true if SkRegion pair are not equivalent + */ + bool operator!=(const SkRegion& other) const { + return !(*this == other); + } + + /** Sets SkRegion to src, and returns true if src bounds is not empty. + This makes SkRegion and src identical by value. Internally, + SkRegion and src share pointer values. The underlying SkRect array is + copied when modified. + + Creating a SkRegion copy is very efficient and never allocates memory. + SkRegion are always copied by value from the interface; the underlying shared + pointers are not exposed. + + @param src SkRegion to copy + @return copy of src + */ + bool set(const SkRegion& src) { + *this = src; + return !this->isEmpty(); + } + + /** Exchanges SkIRect array of SkRegion and other. swap() internally exchanges pointers, + so it is lightweight and does not allocate memory. + + swap() usage has largely been replaced by operator=(const SkRegion& region). + SkPath do not copy their content on assignment until they are written to, + making assignment as efficient as swap(). + + @param other operator=(const SkRegion& region) set + + example: https://fiddle.skia.org/c/@Region_swap + */ + void swap(SkRegion& other); + + /** Returns true if SkRegion is empty. + Empty SkRegion has bounds width or height less than or equal to zero. + SkRegion() constructs empty SkRegion; setEmpty() + and setRect() with dimensionless data make SkRegion empty. + + @return true if bounds has no width or height + */ + bool isEmpty() const { return fRunHead == emptyRunHeadPtr(); } + + /** Returns true if SkRegion is one SkIRect with positive dimensions. + + @return true if SkRegion contains one SkIRect + */ + bool isRect() const { return fRunHead == kRectRunHeadPtr; } + + /** Returns true if SkRegion is described by more than one rectangle. + + @return true if SkRegion contains more than one SkIRect + */ + bool isComplex() const { return !this->isEmpty() && !this->isRect(); } + + /** Returns minimum and maximum axes values of SkIRect array. + Returns (0, 0, 0, 0) if SkRegion is empty. + + @return combined bounds of all SkIRect elements + */ + const SkIRect& getBounds() const { return fBounds; } + + /** Returns a value that increases with the number of + elements in SkRegion. Returns zero if SkRegion is empty. + Returns one if SkRegion equals SkIRect; otherwise, returns + value greater than one indicating that SkRegion is complex. + + Call to compare SkRegion for relative complexity. + + @return relative complexity + + example: https://fiddle.skia.org/c/@Region_computeRegionComplexity + */ + int computeRegionComplexity() const; + + /** Appends outline of SkRegion to path. + Returns true if SkRegion is not empty; otherwise, returns false, and leaves path + unmodified. + + @param path SkPath to append to + @return true if path changed + + example: https://fiddle.skia.org/c/@Region_getBoundaryPath + */ + bool getBoundaryPath(SkPath* path) const; + + /** Constructs an empty SkRegion. SkRegion is set to empty bounds + at (0, 0) with zero width and height. Always returns false. + + @return false + + example: https://fiddle.skia.org/c/@Region_setEmpty + */ + bool setEmpty(); + + /** Constructs a rectangular SkRegion matching the bounds of rect. + If rect is empty, constructs empty and returns false. + + @param rect bounds of constructed SkRegion + @return true if rect is not empty + + example: https://fiddle.skia.org/c/@Region_setRect + */ + bool setRect(const SkIRect& rect); + + /** Constructs SkRegion as the union of SkIRect in rects array. If count is + zero, constructs empty SkRegion. Returns false if constructed SkRegion is empty. + + May be faster than repeated calls to op(). + + @param rects array of SkIRect + @param count array size + @return true if constructed SkRegion is not empty + + example: https://fiddle.skia.org/c/@Region_setRects + */ + bool setRects(const SkIRect rects[], int count); + + /** Constructs a copy of an existing region. + Makes two regions identical by value. Internally, region and + the returned result share pointer values. The underlying SkRect array is + copied when modified. + + Creating a SkRegion copy is very efficient and never allocates memory. + SkRegion are always copied by value from the interface; the underlying shared + pointers are not exposed. + + @param region SkRegion to copy by value + @return SkRegion to copy by value + + example: https://fiddle.skia.org/c/@Region_setRegion + */ + bool setRegion(const SkRegion& region); + + /** Constructs SkRegion to match outline of path within clip. + Returns false if constructed SkRegion is empty. + + Constructed SkRegion draws the same pixels as path through clip when + anti-aliasing is disabled. + + @param path SkPath providing outline + @param clip SkRegion containing path + @return true if constructed SkRegion is not empty + + example: https://fiddle.skia.org/c/@Region_setPath + */ + bool setPath(const SkPath& path, const SkRegion& clip); + + /** Returns true if SkRegion intersects rect. + Returns false if either rect or SkRegion is empty, or do not intersect. + + @param rect SkIRect to intersect + @return true if rect and SkRegion have area in common + + example: https://fiddle.skia.org/c/@Region_intersects + */ + bool intersects(const SkIRect& rect) const; + + /** Returns true if SkRegion intersects other. + Returns false if either other or SkRegion is empty, or do not intersect. + + @param other SkRegion to intersect + @return true if other and SkRegion have area in common + + example: https://fiddle.skia.org/c/@Region_intersects_2 + */ + bool intersects(const SkRegion& other) const; + + /** Returns true if SkIPoint (x, y) is inside SkRegion. + Returns false if SkRegion is empty. + + @param x test SkIPoint x-coordinate + @param y test SkIPoint y-coordinate + @return true if (x, y) is inside SkRegion + + example: https://fiddle.skia.org/c/@Region_contains + */ + bool contains(int32_t x, int32_t y) const; + + /** Returns true if other is completely inside SkRegion. + Returns false if SkRegion or other is empty. + + @param other SkIRect to contain + @return true if other is inside SkRegion + + example: https://fiddle.skia.org/c/@Region_contains_2 + */ + bool contains(const SkIRect& other) const; + + /** Returns true if other is completely inside SkRegion. + Returns false if SkRegion or other is empty. + + @param other SkRegion to contain + @return true if other is inside SkRegion + + example: https://fiddle.skia.org/c/@Region_contains_3 + */ + bool contains(const SkRegion& other) const; + + /** Returns true if SkRegion is a single rectangle and contains r. + May return false even though SkRegion contains r. + + @param r SkIRect to contain + @return true quickly if r points are equal or inside + */ + bool quickContains(const SkIRect& r) const { + SkASSERT(this->isEmpty() == fBounds.isEmpty()); // valid region + + return r.fLeft < r.fRight && r.fTop < r.fBottom && + fRunHead == kRectRunHeadPtr && // this->isRect() + /* fBounds.contains(left, top, right, bottom); */ + fBounds.fLeft <= r.fLeft && fBounds.fTop <= r.fTop && + fBounds.fRight >= r.fRight && fBounds.fBottom >= r.fBottom; + } + + /** Returns true if SkRegion does not intersect rect. + Returns true if rect is empty or SkRegion is empty. + May return false even though SkRegion does not intersect rect. + + @param rect SkIRect to intersect + @return true if rect does not intersect + */ + bool quickReject(const SkIRect& rect) const { + return this->isEmpty() || rect.isEmpty() || + !SkIRect::Intersects(fBounds, rect); + } + + /** Returns true if SkRegion does not intersect rgn. + Returns true if rgn is empty or SkRegion is empty. + May return false even though SkRegion does not intersect rgn. + + @param rgn SkRegion to intersect + @return true if rgn does not intersect + */ + bool quickReject(const SkRegion& rgn) const { + return this->isEmpty() || rgn.isEmpty() || + !SkIRect::Intersects(fBounds, rgn.fBounds); + } + + /** Offsets SkRegion by ivector (dx, dy). Has no effect if SkRegion is empty. + + @param dx x-axis offset + @param dy y-axis offset + */ + void translate(int dx, int dy) { this->translate(dx, dy, this); } + + /** Offsets SkRegion by ivector (dx, dy), writing result to dst. SkRegion may be passed + as dst parameter, translating SkRegion in place. Has no effect if dst is nullptr. + If SkRegion is empty, sets dst to empty. + + @param dx x-axis offset + @param dy y-axis offset + @param dst translated result + + example: https://fiddle.skia.org/c/@Region_translate_2 + */ + void translate(int dx, int dy, SkRegion* dst) const; + + /** \enum SkRegion::Op + The logical operations that can be performed when combining two SkRegion. + */ + enum Op { + kDifference_Op, //!< target minus operand + kIntersect_Op, //!< target intersected with operand + kUnion_Op, //!< target unioned with operand + kXOR_Op, //!< target exclusive or with operand + kReverseDifference_Op, //!< operand minus target + kReplace_Op, //!< replace target with operand + kLastOp = kReplace_Op, //!< last operator + }; + + static const int kOpCnt = kLastOp + 1; + + /** Replaces SkRegion with the result of SkRegion op rect. + Returns true if replaced SkRegion is not empty. + + @param rect SkIRect operand + @return false if result is empty + */ + bool op(const SkIRect& rect, Op op) { + if (this->isRect() && kIntersect_Op == op) { + if (!fBounds.intersect(rect)) { + return this->setEmpty(); + } + return true; + } + return this->op(*this, rect, op); + } + + /** Replaces SkRegion with the result of SkRegion op rgn. + Returns true if replaced SkRegion is not empty. + + @param rgn SkRegion operand + @return false if result is empty + */ + bool op(const SkRegion& rgn, Op op) { return this->op(*this, rgn, op); } + + /** Replaces SkRegion with the result of rect op rgn. + Returns true if replaced SkRegion is not empty. + + @param rect SkIRect operand + @param rgn SkRegion operand + @return false if result is empty + + example: https://fiddle.skia.org/c/@Region_op_4 + */ + bool op(const SkIRect& rect, const SkRegion& rgn, Op op); + + /** Replaces SkRegion with the result of rgn op rect. + Returns true if replaced SkRegion is not empty. + + @param rgn SkRegion operand + @param rect SkIRect operand + @return false if result is empty + + example: https://fiddle.skia.org/c/@Region_op_5 + */ + bool op(const SkRegion& rgn, const SkIRect& rect, Op op); + + /** Replaces SkRegion with the result of rgna op rgnb. + Returns true if replaced SkRegion is not empty. + + @param rgna SkRegion operand + @param rgnb SkRegion operand + @return false if result is empty + + example: https://fiddle.skia.org/c/@Region_op_6 + */ + bool op(const SkRegion& rgna, const SkRegion& rgnb, Op op); + +#ifdef SK_BUILD_FOR_ANDROID_FRAMEWORK + /** Private. Android framework only. + + @return string representation of SkRegion + */ + char* toString(); +#endif + + /** \class SkRegion::Iterator + Returns sequence of rectangles, sorted along y-axis, then x-axis, that make + up SkRegion. + */ + class SK_API Iterator { + public: + + /** Initializes SkRegion::Iterator with an empty SkRegion. done() on SkRegion::Iterator + returns true. + Call reset() to initialized SkRegion::Iterator at a later time. + + @return empty SkRegion iterator + */ + Iterator() : fRgn(nullptr), fDone(true) {} + + /** Sets SkRegion::Iterator to return elements of SkIRect array in region. + + @param region SkRegion to iterate + @return SkRegion iterator + + example: https://fiddle.skia.org/c/@Region_Iterator_copy_const_SkRegion + */ + Iterator(const SkRegion& region); + + /** SkPoint SkRegion::Iterator to start of SkRegion. + Returns true if SkRegion was set; otherwise, returns false. + + @return true if SkRegion was set + + example: https://fiddle.skia.org/c/@Region_Iterator_rewind + */ + bool rewind(); + + /** Resets iterator, using the new SkRegion. + + @param region SkRegion to iterate + + example: https://fiddle.skia.org/c/@Region_Iterator_reset + */ + void reset(const SkRegion& region); + + /** Returns true if SkRegion::Iterator is pointing to final SkIRect in SkRegion. + + @return true if data parsing is complete + */ + bool done() const { return fDone; } + + /** Advances SkRegion::Iterator to next SkIRect in SkRegion if it is not done. + + example: https://fiddle.skia.org/c/@Region_Iterator_next + */ + void next(); + + /** Returns SkIRect element in SkRegion. Does not return predictable results if SkRegion + is empty. + + @return part of SkRegion as SkIRect + */ + const SkIRect& rect() const { return fRect; } + + /** Returns SkRegion if set; otherwise, returns nullptr. + + @return iterated SkRegion + */ + const SkRegion* rgn() const { return fRgn; } + + private: + const SkRegion* fRgn; + const SkRegion::RunType* fRuns; + SkIRect fRect = {0, 0, 0, 0}; + bool fDone; + }; + + /** \class SkRegion::Cliperator + Returns the sequence of rectangles, sorted along y-axis, then x-axis, that make + up SkRegion intersected with the specified clip rectangle. + */ + class SK_API Cliperator { + public: + + /** Sets SkRegion::Cliperator to return elements of SkIRect array in SkRegion within clip. + + @param region SkRegion to iterate + @param clip bounds of iteration + @return SkRegion iterator + + example: https://fiddle.skia.org/c/@Region_Cliperator_const_SkRegion_const_SkIRect + */ + Cliperator(const SkRegion& region, const SkIRect& clip); + + /** Returns true if SkRegion::Cliperator is pointing to final SkIRect in SkRegion. + + @return true if data parsing is complete + */ + bool done() { return fDone; } + + /** Advances iterator to next SkIRect in SkRegion contained by clip. + + example: https://fiddle.skia.org/c/@Region_Cliperator_next + */ + void next(); + + /** Returns SkIRect element in SkRegion, intersected with clip passed to + SkRegion::Cliperator constructor. Does not return predictable results if SkRegion + is empty. + + @return part of SkRegion inside clip as SkIRect + */ + const SkIRect& rect() const { return fRect; } + + private: + Iterator fIter; + SkIRect fClip; + SkIRect fRect = {0, 0, 0, 0}; + bool fDone; + }; + + /** \class SkRegion::Spanerator + Returns the line segment ends within SkRegion that intersect a horizontal line. + */ + class Spanerator { + public: + + /** Sets SkRegion::Spanerator to return line segments in SkRegion on scan line. + + @param region SkRegion to iterate + @param y horizontal line to intersect + @param left bounds of iteration + @param right bounds of iteration + @return SkRegion iterator + + example: https://fiddle.skia.org/c/@Region_Spanerator_const_SkRegion_int_int_int + */ + Spanerator(const SkRegion& region, int y, int left, int right); + + /** Advances iterator to next span intersecting SkRegion within line segment provided + in constructor. Returns true if interval was found. + + @param left pointer to span start; may be nullptr + @param right pointer to span end; may be nullptr + @return true if interval was found + + example: https://fiddle.skia.org/c/@Region_Spanerator_next + */ + bool next(int* left, int* right); + + private: + const SkRegion::RunType* fRuns; + int fLeft, fRight; + bool fDone; + }; + + /** Writes SkRegion to buffer, and returns number of bytes written. + If buffer is nullptr, returns number number of bytes that would be written. + + @param buffer storage for binary data + @return size of SkRegion + + example: https://fiddle.skia.org/c/@Region_writeToMemory + */ + size_t writeToMemory(void* buffer) const; + + /** Constructs SkRegion from buffer of size length. Returns bytes read. + Returned value will be multiple of four or zero if length was too small. + + @param buffer storage for binary data + @param length size of buffer + @return bytes read + + example: https://fiddle.skia.org/c/@Region_readFromMemory + */ + size_t readFromMemory(const void* buffer, size_t length); + + using sk_is_trivially_relocatable = std::true_type; + +private: + static constexpr int kOpCount = kReplace_Op + 1; + + // T + // [B N L R S] + // S + static constexpr int kRectRegionRuns = 7; + + struct RunHead; + + static RunHead* emptyRunHeadPtr() { return (SkRegion::RunHead*) -1; } + static constexpr RunHead* kRectRunHeadPtr = nullptr; + + // allocate space for count runs + void allocateRuns(int count); + void allocateRuns(int count, int ySpanCount, int intervalCount); + void allocateRuns(const RunHead& src); + + SkDEBUGCODE(void dump() const;) + + SkIRect fBounds; + RunHead* fRunHead; + + static_assert(::sk_is_trivially_relocatable::value); + static_assert(::sk_is_trivially_relocatable::value); + + void freeRuns(); + + /** + * Return the runs from this region, consing up fake runs if the region + * is empty or a rect. In those 2 cases, we use tmpStorage to hold the + * run data. + */ + const RunType* getRuns(RunType tmpStorage[], int* intervals) const; + + // This is called with runs[] that do not yet have their interval-count + // field set on each scanline. That is computed as part of this call + // (inside ComputeRunBounds). + bool setRuns(RunType runs[], int count); + + int count_runtype_values(int* itop, int* ibot) const; + + bool isValid() const; + + static void BuildRectRuns(const SkIRect& bounds, + RunType runs[kRectRegionRuns]); + + // If the runs define a simple rect, return true and set bounds to that + // rect. If not, return false and ignore bounds. + static bool RunsAreARect(const SkRegion::RunType runs[], int count, + SkIRect* bounds); + + /** + * If the last arg is null, just return if the result is non-empty, + * else store the result in the last arg. + */ + static bool Oper(const SkRegion&, const SkRegion&, SkRegion::Op, SkRegion*); + + friend struct RunHead; + friend class Iterator; + friend class Spanerator; + friend class SkRegionPriv; + friend class SkRgnBuilder; + friend class SkFlatRegion; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSamplingOptions.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSamplingOptions.h new file mode 100644 index 00000000000..4531a8c9494 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSamplingOptions.h @@ -0,0 +1,107 @@ +/* + * Copyright 2020 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkImageSampling_DEFINED +#define SkImageSampling_DEFINED + +#include "include/core/SkTypes.h" + +#include +#include + +enum class SkFilterMode { + kNearest, // single sample point (nearest neighbor) + kLinear, // interporate between 2x2 sample points (bilinear interpolation) + + kLast = kLinear, +}; +static constexpr int kSkFilterModeCount = static_cast(SkFilterMode::kLast) + 1; + +enum class SkMipmapMode { + kNone, // ignore mipmap levels, sample from the "base" + kNearest, // sample from the nearest level + kLinear, // interpolate between the two nearest levels + + kLast = kLinear, +}; +static constexpr int kSkMipmapModeCount = static_cast(SkMipmapMode::kLast) + 1; + +/* + * Specify B and C (each between 0...1) to create a shader that applies the corresponding + * cubic reconstruction filter to the image. + * + * Example values: + * B = 1/3, C = 1/3 "Mitchell" filter + * B = 0, C = 1/2 "Catmull-Rom" filter + * + * See "Reconstruction Filters in Computer Graphics" + * Don P. Mitchell + * Arun N. Netravali + * 1988 + * https://www.cs.utexas.edu/~fussell/courses/cs384g-fall2013/lectures/mitchell/Mitchell.pdf + * + * Desmos worksheet https://www.desmos.com/calculator/aghdpicrvr + * Nice overview https://entropymine.com/imageworsener/bicubic/ + */ +struct SkCubicResampler { + float B, C; + + // Historic default for kHigh_SkFilterQuality + static constexpr SkCubicResampler Mitchell() { return {1/3.0f, 1/3.0f}; } + static constexpr SkCubicResampler CatmullRom() { return {0.0f, 1/2.0f}; } +}; + +struct SK_API SkSamplingOptions { + const int maxAniso = 0; + const bool useCubic = false; + const SkCubicResampler cubic = {0, 0}; + const SkFilterMode filter = SkFilterMode::kNearest; + const SkMipmapMode mipmap = SkMipmapMode::kNone; + + constexpr SkSamplingOptions() = default; + SkSamplingOptions(const SkSamplingOptions&) = default; + SkSamplingOptions& operator=(const SkSamplingOptions& that) { + this->~SkSamplingOptions(); // A pedantic no-op. + new (this) SkSamplingOptions(that); + return *this; + } + + constexpr SkSamplingOptions(SkFilterMode fm, SkMipmapMode mm) + : filter(fm) + , mipmap(mm) {} + + // These are intentionally implicit because the single parameter clearly conveys what the + // implicitly created SkSamplingOptions will be. + constexpr SkSamplingOptions(SkFilterMode fm) + : filter(fm) + , mipmap(SkMipmapMode::kNone) {} + + constexpr SkSamplingOptions(const SkCubicResampler& c) + : useCubic(true) + , cubic(c) {} + + static constexpr SkSamplingOptions Aniso(int maxAniso) { + return SkSamplingOptions{std::max(maxAniso, 1)}; + } + + bool operator==(const SkSamplingOptions& other) const { + return maxAniso == other.maxAniso + && useCubic == other.useCubic + && cubic.B == other.cubic.B + && cubic.C == other.cubic.C + && filter == other.filter + && mipmap == other.mipmap; + } + bool operator!=(const SkSamplingOptions& other) const { return !(*this == other); } + + bool isAniso() const { return maxAniso != 0; } + +private: + constexpr SkSamplingOptions(int maxAniso) : maxAniso(maxAniso) {} +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkScalar.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkScalar.h new file mode 100644 index 00000000000..d4150f6fb06 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkScalar.h @@ -0,0 +1,161 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkScalar_DEFINED +#define SkScalar_DEFINED + +#include "include/private/base/SkAssert.h" +#include "include/private/base/SkFloatingPoint.h" + +#include + +typedef float SkScalar; + +#define SK_Scalar1 1.0f +#define SK_ScalarHalf 0.5f +#define SK_ScalarSqrt2 SK_FloatSqrt2 +#define SK_ScalarPI SK_FloatPI +#define SK_ScalarTanPIOver8 0.414213562f +#define SK_ScalarRoot2Over2 0.707106781f +#define SK_ScalarMax 3.402823466e+38f +#define SK_ScalarMin (-SK_ScalarMax) +#define SK_ScalarInfinity SK_FloatInfinity +#define SK_ScalarNegativeInfinity SK_FloatNegativeInfinity +#define SK_ScalarNaN SK_FloatNaN + +#define SkScalarFloorToScalar(x) std::floor(x) +#define SkScalarCeilToScalar(x) std::ceil(x) +#define SkScalarRoundToScalar(x) sk_float_round(x) +#define SkScalarTruncToScalar(x) std::trunc(x) + +#define SkScalarFloorToInt(x) sk_float_floor2int(x) +#define SkScalarCeilToInt(x) sk_float_ceil2int(x) +#define SkScalarRoundToInt(x) sk_float_round2int(x) + +#define SkScalarAbs(x) std::fabs(x) +#define SkScalarCopySign(x, y) std::copysign(x, y) +#define SkScalarMod(x, y) std::fmod(x,y) +#define SkScalarSqrt(x) std::sqrt(x) +#define SkScalarPow(b, e) std::pow(b, e) + +#define SkScalarSin(radians) ((float)std::sin(radians)) +#define SkScalarCos(radians) ((float)std::cos(radians)) +#define SkScalarTan(radians) ((float)std::tan(radians)) +#define SkScalarASin(val) ((float)std::asin(val)) +#define SkScalarACos(val) ((float)std::acos(val)) +#define SkScalarATan2(y, x) ((float)std::atan2(y,x)) +#define SkScalarExp(x) ((float)std::exp(x)) +#define SkScalarLog(x) ((float)std::log(x)) +#define SkScalarLog2(x) ((float)std::log2(x)) + +////////////////////////////////////////////////////////////////////////////////////////////////// + +#define SkIntToScalar(x) static_cast(x) +#define SkIntToFloat(x) static_cast(x) +#define SkScalarTruncToInt(x) sk_float_saturate2int(x) + +#define SkScalarToFloat(x) static_cast(x) +#define SkFloatToScalar(x) static_cast(x) +#define SkScalarToDouble(x) static_cast(x) +#define SkDoubleToScalar(x) sk_double_to_float(x) + +/** Returns the fractional part of the scalar. */ +static inline SkScalar SkScalarFraction(SkScalar x) { + return x - SkScalarTruncToScalar(x); +} + +static inline SkScalar SkScalarSquare(SkScalar x) { return x * x; } + +#define SkScalarInvert(x) (SK_Scalar1 / (x)) +#define SkScalarAve(a, b) (((a) + (b)) * SK_ScalarHalf) +#define SkScalarHalf(a) ((a) * SK_ScalarHalf) + +#define SkDegreesToRadians(degrees) ((degrees) * (SK_ScalarPI / 180)) +#define SkRadiansToDegrees(radians) ((radians) * (180 / SK_ScalarPI)) + +static inline bool SkScalarIsInt(SkScalar x) { + return x == SkScalarFloorToScalar(x); +} + +/** + * Returns -1 || 0 || 1 depending on the sign of value: + * -1 if x < 0 + * 0 if x == 0 + * 1 if x > 0 + */ +static inline int SkScalarSignAsInt(SkScalar x) { + return x < 0 ? -1 : (x > 0); +} + +// Scalar result version of above +static inline SkScalar SkScalarSignAsScalar(SkScalar x) { + return x < 0 ? -SK_Scalar1 : ((x > 0) ? SK_Scalar1 : 0); +} + +#define SK_ScalarNearlyZero (SK_Scalar1 / (1 << 12)) + +static inline bool SkScalarNearlyZero(SkScalar x, + SkScalar tolerance = SK_ScalarNearlyZero) { + SkASSERT(tolerance >= 0); + return SkScalarAbs(x) <= tolerance; +} + +static inline bool SkScalarNearlyEqual(SkScalar x, SkScalar y, + SkScalar tolerance = SK_ScalarNearlyZero) { + SkASSERT(tolerance >= 0); + return SkScalarAbs(x-y) <= tolerance; +} + +#define SK_ScalarSinCosNearlyZero (SK_Scalar1 / (1 << 16)) + +static inline float SkScalarSinSnapToZero(SkScalar radians) { + float v = SkScalarSin(radians); + return SkScalarNearlyZero(v, SK_ScalarSinCosNearlyZero) ? 0.0f : v; +} + +static inline float SkScalarCosSnapToZero(SkScalar radians) { + float v = SkScalarCos(radians); + return SkScalarNearlyZero(v, SK_ScalarSinCosNearlyZero) ? 0.0f : v; +} + +/** Linearly interpolate between A and B, based on t. + If t is 0, return A + If t is 1, return B + else interpolate. + t must be [0..SK_Scalar1] +*/ +static inline SkScalar SkScalarInterp(SkScalar A, SkScalar B, SkScalar t) { + SkASSERT(t >= 0 && t <= SK_Scalar1); + return A + (B - A) * t; +} + +/** Interpolate along the function described by (keys[length], values[length]) + for the passed searchKey. SearchKeys outside the range keys[0]-keys[Length] + clamp to the min or max value. This function assumes the number of pairs + (length) will be small and a linear search is used. + + Repeated keys are allowed for discontinuous functions (so long as keys is + monotonically increasing). If key is the value of a repeated scalar in + keys the first one will be used. +*/ +SkScalar SkScalarInterpFunc(SkScalar searchKey, const SkScalar keys[], + const SkScalar values[], int length); + +/* + * Helper to compare an array of scalars. + */ +static inline bool SkScalarsEqual(const SkScalar a[], const SkScalar b[], int n) { + SkASSERT(n >= 0); + for (int i = 0; i < n; ++i) { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSerialProcs.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSerialProcs.h new file mode 100644 index 00000000000..813488d0a71 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSerialProcs.h @@ -0,0 +1,117 @@ +/* + * Copyright 2017 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkSerialProcs_DEFINED +#define SkSerialProcs_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +#include +#include + +class SkData; +class SkImage; +class SkPicture; +class SkTypeface; +class SkReadBuffer; +enum SkAlphaType : int; +namespace sktext::gpu { + class Slug; +} + +/** + * A serial-proc is asked to serialize the specified object (e.g. picture or image). + * If a data object is returned, it will be used (even if it is zero-length). + * If null is returned, then Skia will take its default action. + * + * The default action for pictures is to use Skia's internal format. + * The default action for images is to encode either in its native format or PNG. + * The default action for typefaces is to use Skia's internal format. + */ + +using SkSerialPictureProc = sk_sp (*)(SkPicture*, void* ctx); +using SkSerialImageProc = sk_sp (*)(SkImage*, void* ctx); +using SkSerialTypefaceProc = sk_sp (*)(SkTypeface*, void* ctx); + +/** + * Called with the encoded form of a picture (previously written with a custom + * SkSerialPictureProc proc). Return a picture object, or nullptr indicating failure. + */ +using SkDeserialPictureProc = sk_sp (*)(const void* data, size_t length, void* ctx); + +/** + * Called with the encoded form of an image. The proc can return an image object, or if it + * returns nullptr, then Skia will take its default action to try to create an image from the data. + * + * This will also be used to decode the internal mipmap layers that are saved on some images. + * + * An explicit SkAlphaType may have been encoded in the bytestream; if not, then the passed in + * optional will be not present. + * + * Clients should set at least SkDeserialImageProc; SkDeserialImageFromDataProc may be called + * if the internal implementation has a SkData copy already. Implementations of SkDeserialImageProc + * must make a copy of any data they needed after the proc finishes, since the data will go away + * after serialization ends. + */ +#if !defined(SK_LEGACY_DESERIAL_IMAGE_PROC) +using SkDeserialImageProc = sk_sp (*)(const void* data, size_t length, void* ctx); +#else +using SkDeserialImageProc = sk_sp (*)(const void* data, + size_t length, + std::optional, + void* ctx); +#endif +using SkDeserialImageFromDataProc = sk_sp (*)(sk_sp, + std::optional, + void* ctx); + +/** + * Slugs are currently only deserializable with a GPU backend. Clients will not be able to + * provide a custom mechanism here, but can enable Slug deserialization by calling + * sktext::gpu::AddDeserialProcs to add Skia's implementation. + */ +using SkSlugProc = sk_sp (*)(SkReadBuffer&, void* ctx); + +/** + * Called with the encoded form of a typeface (previously written with a custom + * SkSerialTypefaceProc proc). Return a typeface object, or nullptr indicating failure. + */ +using SkDeserialTypefaceProc = sk_sp (*)(const void* data, size_t length, void* ctx); + +struct SK_API SkSerialProcs { + SkSerialPictureProc fPictureProc = nullptr; + void* fPictureCtx = nullptr; + + SkSerialImageProc fImageProc = nullptr; + void* fImageCtx = nullptr; + + SkSerialTypefaceProc fTypefaceProc = nullptr; + void* fTypefaceCtx = nullptr; +}; + +struct SK_API SkDeserialProcs { + SkDeserialPictureProc fPictureProc = nullptr; + void* fPictureCtx = nullptr; + + SkDeserialImageProc fImageProc = nullptr; + SkDeserialImageFromDataProc fImageDataProc = nullptr; + void* fImageCtx = nullptr; + + SkSlugProc fSlugProc = nullptr; + void* fSlugCtx = nullptr; + + SkDeserialTypefaceProc fTypefaceProc = nullptr; + void* fTypefaceCtx = nullptr; + + // This looks like a flag, but it could be considered a proc as well (one that takes no + // parameters and returns a bool). Given that there are only two valid implementations of that + // proc, we just insert the bool directly. + bool fAllowSkSL = true; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkShader.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkShader.h new file mode 100644 index 00000000000..b650a03d7b0 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkShader.h @@ -0,0 +1,112 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkShader_DEFINED +#define SkShader_DEFINED + +#include "include/core/SkColor.h" +#include "include/core/SkFlattenable.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +class SkBlender; +class SkColorFilter; +class SkColorSpace; +class SkImage; +class SkMatrix; +enum class SkBlendMode; +enum class SkTileMode; +struct SkRect; +struct SkSamplingOptions; + +/** \class SkShader + * + * Shaders specify the source color(s) for what is being drawn. If a paint + * has no shader, then the paint's color is used. If the paint has a + * shader, then the shader's color(s) are use instead, but they are + * modulated by the paint's alpha. This makes it easy to create a shader + * once (e.g. bitmap tiling or gradient) and then change its transparency + * w/o having to modify the original shader... only the paint's alpha needs + * to be modified. + */ +class SK_API SkShader : public SkFlattenable { +public: + /** + * Returns true if the shader is guaranteed to produce only opaque + * colors, subject to the SkPaint using the shader to apply an opaque + * alpha value. Subclasses should override this to allow some + * optimizations. + */ + virtual bool isOpaque() const { return false; } + + /** + * Iff this shader is backed by a single SkImage, return its ptr (the caller must ref this + * if they want to keep it longer than the lifetime of the shader). If not, return nullptr. + */ + SkImage* isAImage(SkMatrix* localMatrix, SkTileMode xy[2]) const; + + bool isAImage() const { + return this->isAImage(nullptr, (SkTileMode*)nullptr) != nullptr; + } + + ////////////////////////////////////////////////////////////////////////// + // Methods to create combinations or variants of shaders + + /** + * Return a shader that will apply the specified localMatrix to this shader. + * The specified matrix will be applied before any matrix associated with this shader. + */ + sk_sp makeWithLocalMatrix(const SkMatrix&) const; + + /** + * Create a new shader that produces the same colors as invoking this shader and then applying + * the colorfilter. + */ + sk_sp makeWithColorFilter(sk_sp) const; + + /** + * Return a shader that will compute this shader in a specific color space. + * By default, all shaders operate in the destination (surface) color space. + * The results of a shader are still always converted to the destination - this + * API has no impact on simple shaders or images. Primarily, it impacts shaders + * that perform mathematical operations, like Blend shaders, or runtime shaders. + */ + sk_sp makeWithWorkingColorSpace(sk_sp) const; + +private: + SkShader() = default; + friend class SkShaderBase; + + using INHERITED = SkFlattenable; +}; + +namespace SkShaders { +SK_API sk_sp Empty(); +SK_API sk_sp Color(SkColor); +SK_API sk_sp Color(const SkColor4f&, sk_sp); +SK_API sk_sp Blend(SkBlendMode mode, sk_sp dst, sk_sp src); +SK_API sk_sp Blend(sk_sp, sk_sp dst, sk_sp src); +SK_API sk_sp CoordClamp(sk_sp, const SkRect& subset); + +/* + * Create an SkShader that will sample the 'image'. This is equivalent to SkImage::makeShader. + */ +SK_API sk_sp Image(sk_sp image, + SkTileMode tmx, SkTileMode tmy, + const SkSamplingOptions& options, + const SkMatrix* localMatrix = nullptr); +/* + * Create an SkShader that will sample 'image' with minimal processing. This is equivalent to + * SkImage::makeRawShader. + */ +SK_API sk_sp RawImage(sk_sp image, + SkTileMode tmx, SkTileMode tmy, + const SkSamplingOptions& options, + const SkMatrix* localMatrix = nullptr); +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSize.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSize.h new file mode 100644 index 00000000000..3be747d274b --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSize.h @@ -0,0 +1,93 @@ +/* + * Copyright 2011 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkSize_DEFINED +#define SkSize_DEFINED + +#include "include/core/SkScalar.h" +#include "include/private/base/SkTo.h" + +#include + +struct SkISize { + int32_t fWidth; + int32_t fHeight; + + static constexpr SkISize Make(int32_t w, int32_t h) { return {w, h}; } + + static constexpr SkISize MakeEmpty() { return {0, 0}; } + + void set(int32_t w, int32_t h) { *this = SkISize{w, h}; } + + /** Returns true iff fWidth == 0 && fHeight == 0 + */ + bool isZero() const { return 0 == fWidth && 0 == fHeight; } + + /** Returns true if either width or height are <= 0 */ + bool isEmpty() const { return fWidth <= 0 || fHeight <= 0; } + + /** Set the width and height to 0 */ + void setEmpty() { fWidth = fHeight = 0; } + + constexpr int32_t width() const { return fWidth; } + constexpr int32_t height() const { return fHeight; } + + constexpr int64_t area() const { return SkToS64(fWidth) * SkToS64(fHeight); } + + bool equals(int32_t w, int32_t h) const { return fWidth == w && fHeight == h; } +}; + +static inline bool operator==(const SkISize& a, const SkISize& b) { + return a.fWidth == b.fWidth && a.fHeight == b.fHeight; +} + +static inline bool operator!=(const SkISize& a, const SkISize& b) { return !(a == b); } + +/////////////////////////////////////////////////////////////////////////////// + +struct SkSize { + SkScalar fWidth; + SkScalar fHeight; + + static constexpr SkSize Make(SkScalar w, SkScalar h) { return {w, h}; } + + static constexpr SkSize Make(const SkISize& src) { + return {SkIntToScalar(src.width()), SkIntToScalar(src.height())}; + } + + static constexpr SkSize MakeEmpty() { return {0, 0}; } + + void set(SkScalar w, SkScalar h) { *this = SkSize{w, h}; } + + /** Returns true iff fWidth == 0 && fHeight == 0 + */ + bool isZero() const { return 0 == fWidth && 0 == fHeight; } + + /** Returns true if either width or height are <= 0 */ + bool isEmpty() const { return fWidth <= 0 || fHeight <= 0; } + + /** Set the width and height to 0 */ + void setEmpty() { *this = SkSize{0, 0}; } + + SkScalar width() const { return fWidth; } + SkScalar height() const { return fHeight; } + + bool equals(SkScalar w, SkScalar h) const { return fWidth == w && fHeight == h; } + + SkISize toRound() const { return {SkScalarRoundToInt(fWidth), SkScalarRoundToInt(fHeight)}; } + + SkISize toCeil() const { return {SkScalarCeilToInt(fWidth), SkScalarCeilToInt(fHeight)}; } + + SkISize toFloor() const { return {SkScalarFloorToInt(fWidth), SkScalarFloorToInt(fHeight)}; } +}; + +static inline bool operator==(const SkSize& a, const SkSize& b) { + return a.fWidth == b.fWidth && a.fHeight == b.fHeight; +} + +static inline bool operator!=(const SkSize& a, const SkSize& b) { return !(a == b); } +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSpan.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSpan.h new file mode 100644 index 00000000000..37cac632b1e --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSpan.h @@ -0,0 +1,13 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +// We want SkSpan to be a public API, but it is also fundamental to many of our internal types. +// Thus, we have a public file that clients can include. This file defers to the private copy +// so we do not have a dependency cycle from our "base" files to our "core" files. + +#include "include/private/base/SkSpan_impl.h" // IWYU pragma: export + diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkStream.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkStream.h new file mode 100644 index 00000000000..208bfe2903c --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkStream.h @@ -0,0 +1,512 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkStream_DEFINED +#define SkStream_DEFINED + +#include "include/core/SkData.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkCPUTypes.h" +#include "include/private/base/SkTo.h" + +#include +#include +#include +#include +#include +class SkStreamAsset; + +/** + * SkStream -- abstraction for a source of bytes. Subclasses can be backed by + * memory, or a file, or something else. + */ +class SK_API SkStream { +public: + virtual ~SkStream() {} + SkStream() {} + + /** + * Attempts to open the specified file as a stream, returns nullptr on failure. + */ + static std::unique_ptr MakeFromFile(const char path[]); + + /** Reads or skips size number of bytes. + * If buffer == NULL, skip size bytes, return how many were skipped. + * If buffer != NULL, copy size bytes into buffer, return how many were copied. + * @param buffer when NULL skip size bytes, otherwise copy size bytes into buffer + * @param size the number of bytes to skip or copy + * @return the number of bytes actually read. + */ + virtual size_t read(void* buffer, size_t size) = 0; + + /** Skip size number of bytes. + * @return the actual number bytes that could be skipped. + */ + size_t skip(size_t size) { + return this->read(nullptr, size); + } + + /** + * Attempt to peek at size bytes. + * If this stream supports peeking, copy min(size, peekable bytes) into + * buffer, and return the number of bytes copied. + * If the stream does not support peeking, or cannot peek any bytes, + * return 0 and leave buffer unchanged. + * The stream is guaranteed to be in the same visible state after this + * call, regardless of success or failure. + * @param buffer Must not be NULL, and must be at least size bytes. Destination + * to copy bytes. + * @param size Number of bytes to copy. + * @return The number of bytes peeked/copied. + */ + virtual size_t peek(void* /*buffer*/, size_t /*size*/) const { return 0; } + + /** Returns true when all the bytes in the stream have been read. + * As SkStream represents synchronous I/O, isAtEnd returns false when the + * final stream length isn't known yet, even when all the bytes available + * so far have been read. + * This may return true early (when there are no more bytes to be read) + * or late (after the first unsuccessful read). + */ + virtual bool isAtEnd() const = 0; + + [[nodiscard]] bool readS8(int8_t*); + [[nodiscard]] bool readS16(int16_t*); + [[nodiscard]] bool readS32(int32_t*); + + [[nodiscard]] bool readU8(uint8_t* i) { return this->readS8((int8_t*)i); } + [[nodiscard]] bool readU16(uint16_t* i) { return this->readS16((int16_t*)i); } + [[nodiscard]] bool readU32(uint32_t* i) { return this->readS32((int32_t*)i); } + + [[nodiscard]] bool readBool(bool* b) { + uint8_t i; + if (!this->readU8(&i)) { return false; } + *b = (i != 0); + return true; + } + [[nodiscard]] bool readScalar(SkScalar*); + [[nodiscard]] bool readPackedUInt(size_t*); + +//SkStreamRewindable + /** Rewinds to the beginning of the stream. Returns true if the stream is known + * to be at the beginning after this call returns. + */ + virtual bool rewind() { return false; } + + /** Duplicates this stream. If this cannot be done, returns NULL. + * The returned stream will be positioned at the beginning of its data. + */ + std::unique_ptr duplicate() const { + return std::unique_ptr(this->onDuplicate()); + } + /** Duplicates this stream. If this cannot be done, returns NULL. + * The returned stream will be positioned the same as this stream. + */ + std::unique_ptr fork() const { + return std::unique_ptr(this->onFork()); + } + +//SkStreamSeekable + /** Returns true if this stream can report its current position. */ + virtual bool hasPosition() const { return false; } + /** Returns the current position in the stream. If this cannot be done, returns 0. */ + virtual size_t getPosition() const { return 0; } + + /** Seeks to an absolute position in the stream. If this cannot be done, returns false. + * If an attempt is made to seek past the end of the stream, the position will be set + * to the end of the stream. + */ + virtual bool seek(size_t /*position*/) { return false; } + + /** Seeks to an relative offset in the stream. If this cannot be done, returns false. + * If an attempt is made to move to a position outside the stream, the position will be set + * to the closest point within the stream (beginning or end). + */ + virtual bool move(long /*offset*/) { return false; } + +//SkStreamAsset + /** Returns true if this stream can report its total length. */ + virtual bool hasLength() const { return false; } + /** Returns the total length of the stream. If this cannot be done, returns 0. */ + virtual size_t getLength() const { return 0; } + +//SkStreamMemory + /** Returns the starting address for the data. If this cannot be done, returns NULL. */ + virtual const void* getMemoryBase() { return nullptr; } + virtual sk_sp getData() const { return nullptr; } + +private: + virtual SkStream* onDuplicate() const { return nullptr; } + virtual SkStream* onFork() const { return nullptr; } + + SkStream(SkStream&&) = delete; + SkStream(const SkStream&) = delete; + SkStream& operator=(SkStream&&) = delete; + SkStream& operator=(const SkStream&) = delete; +}; + +/** SkStreamRewindable is a SkStream for which rewind and duplicate are required. */ +class SK_API SkStreamRewindable : public SkStream { +public: + bool rewind() override = 0; + std::unique_ptr duplicate() const { + return std::unique_ptr(this->onDuplicate()); + } +private: + SkStreamRewindable* onDuplicate() const override = 0; +}; + +/** SkStreamSeekable is a SkStreamRewindable for which position, seek, move, and fork are required. */ +class SK_API SkStreamSeekable : public SkStreamRewindable { +public: + std::unique_ptr duplicate() const { + return std::unique_ptr(this->onDuplicate()); + } + + bool hasPosition() const override { return true; } + size_t getPosition() const override = 0; + bool seek(size_t position) override = 0; + bool move(long offset) override = 0; + + std::unique_ptr fork() const { + return std::unique_ptr(this->onFork()); + } +private: + SkStreamSeekable* onDuplicate() const override = 0; + SkStreamSeekable* onFork() const override = 0; +}; + +/** SkStreamAsset is a SkStreamSeekable for which getLength is required. */ +class SK_API SkStreamAsset : public SkStreamSeekable { +public: + bool hasLength() const override { return true; } + size_t getLength() const override = 0; + + std::unique_ptr duplicate() const { + return std::unique_ptr(this->onDuplicate()); + } + std::unique_ptr fork() const { + return std::unique_ptr(this->onFork()); + } +private: + SkStreamAsset* onDuplicate() const override = 0; + SkStreamAsset* onFork() const override = 0; +}; + +/** SkStreamMemory is a SkStreamAsset for which getMemoryBase is required. */ +class SK_API SkStreamMemory : public SkStreamAsset { +public: + const void* getMemoryBase() override = 0; + + std::unique_ptr duplicate() const { + return std::unique_ptr(this->onDuplicate()); + } + std::unique_ptr fork() const { + return std::unique_ptr(this->onFork()); + } +private: + SkStreamMemory* onDuplicate() const override = 0; + SkStreamMemory* onFork() const override = 0; +}; + +class SK_API SkWStream { +public: + virtual ~SkWStream(); + SkWStream() {} + + /** Called to write bytes to a SkWStream. Returns true on success + @param buffer the address of at least size bytes to be written to the stream + @param size The number of bytes in buffer to write to the stream + @return true on success + */ + virtual bool write(const void* buffer, size_t size) = 0; + virtual void flush(); + + virtual size_t bytesWritten() const = 0; + + // helpers + + bool write8(U8CPU value) { + uint8_t v = SkToU8(value); + return this->write(&v, 1); + } + bool write16(U16CPU value) { + uint16_t v = SkToU16(value); + return this->write(&v, 2); + } + bool write32(uint32_t v) { + return this->write(&v, 4); + } + + bool writeText(const char text[]) { + SkASSERT(text); + return this->write(text, std::strlen(text)); + } + + bool newline() { return this->write("\n", std::strlen("\n")); } + + bool writeDecAsText(int32_t); + bool writeBigDecAsText(int64_t, int minDigits = 0); + bool writeHexAsText(uint32_t, int minDigits = 0); + bool writeScalarAsText(SkScalar); + + bool writeBool(bool v) { return this->write8(v); } + bool writeScalar(SkScalar); + bool writePackedUInt(size_t); + + bool writeStream(SkStream* input, size_t length); + + /** + * This returns the number of bytes in the stream required to store + * 'value'. + */ + static int SizeOfPackedUInt(size_t value); + +private: + SkWStream(const SkWStream&) = delete; + SkWStream& operator=(const SkWStream&) = delete; +}; + +class SK_API SkNullWStream : public SkWStream { +public: + SkNullWStream() : fBytesWritten(0) {} + + bool write(const void* , size_t n) override { fBytesWritten += n; return true; } + void flush() override {} + size_t bytesWritten() const override { return fBytesWritten; } + +private: + size_t fBytesWritten; +}; + +//////////////////////////////////////////////////////////////////////////////////////// + +/** A stream that wraps a C FILE* file stream. */ +class SK_API SkFILEStream : public SkStreamAsset { +public: + /** Initialize the stream by calling sk_fopen on the specified path. + * This internal stream will be closed in the destructor. + */ + explicit SkFILEStream(const char path[] = nullptr); + + /** Initialize the stream with an existing C FILE stream. + * The current position of the C FILE stream will be considered the + * beginning of the SkFILEStream and the current seek end of the FILE will be the end. + * The C FILE stream will be closed in the destructor. + */ + explicit SkFILEStream(FILE* file); + + /** Initialize the stream with an existing C FILE stream. + * The current position of the C FILE stream will be considered the + * beginning of the SkFILEStream and size bytes later will be the end. + * The C FILE stream will be closed in the destructor. + */ + explicit SkFILEStream(FILE* file, size_t size); + + ~SkFILEStream() override; + + static std::unique_ptr Make(const char path[]) { + std::unique_ptr stream(new SkFILEStream(path)); + return stream->isValid() ? std::move(stream) : nullptr; + } + + /** Returns true if the current path could be opened. */ + bool isValid() const { return fFILE != nullptr; } + + /** Close this SkFILEStream. */ + void close(); + + size_t read(void* buffer, size_t size) override; + bool isAtEnd() const override; + + bool rewind() override; + std::unique_ptr duplicate() const { + return std::unique_ptr(this->onDuplicate()); + } + + size_t getPosition() const override; + bool seek(size_t position) override; + bool move(long offset) override; + + std::unique_ptr fork() const { + return std::unique_ptr(this->onFork()); + } + + size_t getLength() const override; + +private: + explicit SkFILEStream(FILE*, size_t size, size_t start); + explicit SkFILEStream(std::shared_ptr, size_t end, size_t start); + explicit SkFILEStream(std::shared_ptr, size_t end, size_t start, size_t current); + + SkStreamAsset* onDuplicate() const override; + SkStreamAsset* onFork() const override; + + std::shared_ptr fFILE; + // My own council will I keep on sizes and offsets. + // These are seek positions in the underling FILE, not offsets into the stream. + size_t fEnd; + size_t fStart; + size_t fCurrent; + + using INHERITED = SkStreamAsset; +}; + +class SK_API SkMemoryStream : public SkStreamMemory { +public: + SkMemoryStream(); + + /** We allocate (and free) the memory. Write to it via getMemoryBase() */ + SkMemoryStream(size_t length); + + /** If copyData is true, the stream makes a private copy of the data. */ + SkMemoryStream(const void* data, size_t length, bool copyData = false); + + /** Creates the stream to read from the specified data */ + SkMemoryStream(sk_sp data); + + /** Returns a stream with a copy of the input data. */ + static std::unique_ptr MakeCopy(const void* data, size_t length); + + /** Returns a stream with a bare pointer reference to the input data. */ + static std::unique_ptr MakeDirect(const void* data, size_t length); + + /** Returns a stream with a shared reference to the input data. */ + static std::unique_ptr Make(sk_sp data); + + /** Resets the stream to the specified data and length, + just like the constructor. + if copyData is true, the stream makes a private copy of the data + */ + virtual void setMemory(const void* data, size_t length, + bool copyData = false); + /** Replace any memory buffer with the specified buffer. The caller + must have allocated data with sk_malloc or sk_realloc, since it + will be freed with sk_free. + */ + void setMemoryOwned(const void* data, size_t length); + + sk_sp getData() const override { return fData; } + void setData(sk_sp data); + + const void* getAtPos(); + + size_t read(void* buffer, size_t size) override; + bool isAtEnd() const override; + + size_t peek(void* buffer, size_t size) const override; + + bool rewind() override; + + std::unique_ptr duplicate() const { + return std::unique_ptr(this->onDuplicate()); + } + + size_t getPosition() const override; + bool seek(size_t position) override; + bool move(long offset) override; + + std::unique_ptr fork() const { + return std::unique_ptr(this->onFork()); + } + + size_t getLength() const override; + + const void* getMemoryBase() override; + +private: + SkMemoryStream* onDuplicate() const override; + SkMemoryStream* onFork() const override; + + sk_sp fData; + size_t fOffset; + + using INHERITED = SkStreamMemory; +}; + +///////////////////////////////////////////////////////////////////////////////////////////// + +class SK_API SkFILEWStream : public SkWStream { +public: + SkFILEWStream(const char path[]); + ~SkFILEWStream() override; + + /** Returns true if the current path could be opened. + */ + bool isValid() const { return fFILE != nullptr; } + + bool write(const void* buffer, size_t size) override; + void flush() override; + void fsync(); + size_t bytesWritten() const override; + +private: + FILE* fFILE; + + using INHERITED = SkWStream; +}; + +class SK_API SkDynamicMemoryWStream : public SkWStream { +public: + SkDynamicMemoryWStream() = default; + SkDynamicMemoryWStream(SkDynamicMemoryWStream&&); + SkDynamicMemoryWStream& operator=(SkDynamicMemoryWStream&&); + ~SkDynamicMemoryWStream() override; + + bool write(const void* buffer, size_t size) override; + size_t bytesWritten() const override; + + bool read(void* buffer, size_t offset, size_t size); + + /** More efficient version of read(dst, 0, bytesWritten()). */ + void copyTo(void* dst) const; + bool writeToStream(SkWStream* dst) const; + + /** Equivalent to copyTo() followed by reset(), but may save memory use. */ + void copyToAndReset(void* dst); + + /** Equivalent to writeToStream() followed by reset(), but may save memory use. */ + bool writeToAndReset(SkWStream* dst); + + /** Equivalent to writeToStream() followed by reset(), but may save memory use. + When the dst is also a SkDynamicMemoryWStream, the implementation is constant time. */ + bool writeToAndReset(SkDynamicMemoryWStream* dst); + + /** Prepend this stream to dst, resetting this. */ + void prependToAndReset(SkDynamicMemoryWStream* dst); + + /** Return the contents as SkData, and then reset the stream. */ + sk_sp detachAsData(); + + /** Reset, returning a reader stream with the current content. */ + std::unique_ptr detachAsStream(); + + /** Reset the stream to its original, empty, state. */ + void reset(); + void padToAlign4(); +private: + struct Block; + Block* fHead = nullptr; + Block* fTail = nullptr; + size_t fBytesWrittenBeforeTail = 0; + +#ifdef SK_DEBUG + void validate() const; +#else + void validate() const {} +#endif + + // For access to the Block type. + friend class SkBlockMemoryStream; + friend class SkBlockMemoryRefCnt; + + using INHERITED = SkWStream; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkString.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkString.h new file mode 100644 index 00000000000..819e14d8581 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkString.h @@ -0,0 +1,293 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkString_DEFINED +#define SkString_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkTo.h" +#include "include/private/base/SkTypeTraits.h" + +#include +#include +#include +#include +#include +#include +#include + +/* Some helper functions for C strings */ +static inline bool SkStrStartsWith(const char string[], const char prefixStr[]) { + SkASSERT(string); + SkASSERT(prefixStr); + return !strncmp(string, prefixStr, strlen(prefixStr)); +} +static inline bool SkStrStartsWith(const char string[], const char prefixChar) { + SkASSERT(string); + return (prefixChar == *string); +} + +bool SkStrEndsWith(const char string[], const char suffixStr[]); +bool SkStrEndsWith(const char string[], const char suffixChar); + +int SkStrStartsWithOneOf(const char string[], const char prefixes[]); + +static inline int SkStrFind(const char string[], const char substring[]) { + const char *first = strstr(string, substring); + if (nullptr == first) return -1; + return SkToInt(first - &string[0]); +} + +static inline int SkStrFindLastOf(const char string[], const char subchar) { + const char* last = strrchr(string, subchar); + if (nullptr == last) return -1; + return SkToInt(last - &string[0]); +} + +static inline bool SkStrContains(const char string[], const char substring[]) { + SkASSERT(string); + SkASSERT(substring); + return (-1 != SkStrFind(string, substring)); +} +static inline bool SkStrContains(const char string[], const char subchar) { + SkASSERT(string); + char tmp[2]; + tmp[0] = subchar; + tmp[1] = '\0'; + return (-1 != SkStrFind(string, tmp)); +} + +/* + * The SkStrAppend... methods will write into the provided buffer, assuming it is large enough. + * Each method has an associated const (e.g. kSkStrAppendU32_MaxSize) which will be the largest + * value needed for that method's buffer. + * + * char storage[kSkStrAppendU32_MaxSize]; + * SkStrAppendU32(storage, value); + * + * Note : none of the SkStrAppend... methods write a terminating 0 to their buffers. Instead, + * the methods return the ptr to the end of the written part of the buffer. This can be used + * to compute the length, and/or know where to write a 0 if that is desired. + * + * char storage[kSkStrAppendU32_MaxSize + 1]; + * char* stop = SkStrAppendU32(storage, value); + * size_t len = stop - storage; + * *stop = 0; // valid, since storage was 1 byte larger than the max. + */ + +static constexpr int kSkStrAppendU32_MaxSize = 10; +char* SkStrAppendU32(char buffer[], uint32_t); +static constexpr int kSkStrAppendU64_MaxSize = 20; +char* SkStrAppendU64(char buffer[], uint64_t, int minDigits); + +static constexpr int kSkStrAppendS32_MaxSize = kSkStrAppendU32_MaxSize + 1; +char* SkStrAppendS32(char buffer[], int32_t); +static constexpr int kSkStrAppendS64_MaxSize = kSkStrAppendU64_MaxSize + 1; +char* SkStrAppendS64(char buffer[], int64_t, int minDigits); + +/** + * Floats have at most 8 significant digits, so we limit our %g to that. + * However, the total string could be 15 characters: -1.2345678e-005 + * + * In theory we should only expect up to 2 digits for the exponent, but on + * some platforms we have seen 3 (as in the example above). + */ +static constexpr int kSkStrAppendScalar_MaxSize = 15; + +/** + * Write the scalar in decimal format into buffer, and return a pointer to + * the next char after the last one written. Note: a terminating 0 is not + * written into buffer, which must be at least kSkStrAppendScalar_MaxSize. + * Thus if the caller wants to add a 0 at the end, buffer must be at least + * kSkStrAppendScalar_MaxSize + 1 bytes large. + */ +char* SkStrAppendScalar(char buffer[], SkScalar); + +/** \class SkString + + Light weight class for managing strings. Uses reference + counting to make string assignments and copies very fast + with no extra RAM cost. Assumes UTF8 encoding. +*/ +class SK_API SkString { +public: + SkString(); + explicit SkString(size_t len); + explicit SkString(const char text[]); + SkString(const char text[], size_t len); + SkString(const SkString&); + SkString(SkString&&); + explicit SkString(const std::string&); + explicit SkString(std::string_view); + ~SkString(); + + bool isEmpty() const { return 0 == fRec->fLength; } + size_t size() const { return (size_t) fRec->fLength; } + const char* data() const { return fRec->data(); } + const char* c_str() const { return fRec->data(); } + char operator[](size_t n) const { return this->c_str()[n]; } + + bool equals(const SkString&) const; + bool equals(const char text[]) const; + bool equals(const char text[], size_t len) const; + + bool startsWith(const char prefixStr[]) const { + return SkStrStartsWith(fRec->data(), prefixStr); + } + bool startsWith(const char prefixChar) const { + return SkStrStartsWith(fRec->data(), prefixChar); + } + bool endsWith(const char suffixStr[]) const { + return SkStrEndsWith(fRec->data(), suffixStr); + } + bool endsWith(const char suffixChar) const { + return SkStrEndsWith(fRec->data(), suffixChar); + } + bool contains(const char substring[]) const { + return SkStrContains(fRec->data(), substring); + } + bool contains(const char subchar) const { + return SkStrContains(fRec->data(), subchar); + } + int find(const char substring[]) const { + return SkStrFind(fRec->data(), substring); + } + int findLastOf(const char subchar) const { + return SkStrFindLastOf(fRec->data(), subchar); + } + + friend bool operator==(const SkString& a, const SkString& b) { + return a.equals(b); + } + friend bool operator!=(const SkString& a, const SkString& b) { + return !a.equals(b); + } + + // these methods edit the string + + SkString& operator=(const SkString&); + SkString& operator=(SkString&&); + SkString& operator=(const char text[]); + + char* data(); + char& operator[](size_t n) { return this->data()[n]; } + + void reset(); + /** String contents are preserved on resize. (For destructive resize, `set(nullptr, length)`.) + * `resize` automatically reserves an extra byte at the end of the buffer for a null terminator. + */ + void resize(size_t len); + void set(const SkString& src) { *this = src; } + void set(const char text[]); + void set(const char text[], size_t len); + void set(std::string_view str) { this->set(str.data(), str.size()); } + + void insert(size_t offset, const char text[]); + void insert(size_t offset, const char text[], size_t len); + void insert(size_t offset, const SkString& str) { this->insert(offset, str.c_str(), str.size()); } + void insert(size_t offset, std::string_view str) { this->insert(offset, str.data(), str.size()); } + void insertUnichar(size_t offset, SkUnichar); + void insertS32(size_t offset, int32_t value); + void insertS64(size_t offset, int64_t value, int minDigits = 0); + void insertU32(size_t offset, uint32_t value); + void insertU64(size_t offset, uint64_t value, int minDigits = 0); + void insertHex(size_t offset, uint32_t value, int minDigits = 0); + void insertScalar(size_t offset, SkScalar); + + void append(const char text[]) { this->insert((size_t)-1, text); } + void append(const char text[], size_t len) { this->insert((size_t)-1, text, len); } + void append(const SkString& str) { this->insert((size_t)-1, str.c_str(), str.size()); } + void append(std::string_view str) { this->insert((size_t)-1, str.data(), str.size()); } + void appendUnichar(SkUnichar uni) { this->insertUnichar((size_t)-1, uni); } + void appendS32(int32_t value) { this->insertS32((size_t)-1, value); } + void appendS64(int64_t value, int minDigits = 0) { this->insertS64((size_t)-1, value, minDigits); } + void appendU32(uint32_t value) { this->insertU32((size_t)-1, value); } + void appendU64(uint64_t value, int minDigits = 0) { this->insertU64((size_t)-1, value, minDigits); } + void appendHex(uint32_t value, int minDigits = 0) { this->insertHex((size_t)-1, value, minDigits); } + void appendScalar(SkScalar value) { this->insertScalar((size_t)-1, value); } + + void prepend(const char text[]) { this->insert(0, text); } + void prepend(const char text[], size_t len) { this->insert(0, text, len); } + void prepend(const SkString& str) { this->insert(0, str.c_str(), str.size()); } + void prepend(std::string_view str) { this->insert(0, str.data(), str.size()); } + void prependUnichar(SkUnichar uni) { this->insertUnichar(0, uni); } + void prependS32(int32_t value) { this->insertS32(0, value); } + void prependS64(int32_t value, int minDigits = 0) { this->insertS64(0, value, minDigits); } + void prependHex(uint32_t value, int minDigits = 0) { this->insertHex(0, value, minDigits); } + void prependScalar(SkScalar value) { this->insertScalar((size_t)-1, value); } + + void printf(const char format[], ...) SK_PRINTF_LIKE(2, 3); + void printVAList(const char format[], va_list) SK_PRINTF_LIKE(2, 0); + void appendf(const char format[], ...) SK_PRINTF_LIKE(2, 3); + void appendVAList(const char format[], va_list) SK_PRINTF_LIKE(2, 0); + void prependf(const char format[], ...) SK_PRINTF_LIKE(2, 3); + void prependVAList(const char format[], va_list) SK_PRINTF_LIKE(2, 0); + + void remove(size_t offset, size_t length); + + SkString& operator+=(const SkString& s) { this->append(s); return *this; } + SkString& operator+=(const char text[]) { this->append(text); return *this; } + SkString& operator+=(const char c) { this->append(&c, 1); return *this; } + + /** + * Swap contents between this and other. This function is guaranteed + * to never fail or throw. + */ + void swap(SkString& other); + + using sk_is_trivially_relocatable = std::true_type; + +private: + struct Rec { + public: + constexpr Rec(uint32_t len, int32_t refCnt) : fLength(len), fRefCnt(refCnt) {} + static sk_sp Make(const char text[], size_t len); + char* data() { return fBeginningOfData; } + const char* data() const { return fBeginningOfData; } + void ref() const; + void unref() const; + bool unique() const; +#ifdef SK_DEBUG + int32_t getRefCnt() const; +#endif + uint32_t fLength; // logically size_t, but we want it to stay 32 bits + + private: + mutable std::atomic fRefCnt; + char fBeginningOfData[1] = {'\0'}; + + // Ensure the unsized delete is called. + void operator delete(void* p) { ::operator delete(p); } + }; + sk_sp fRec; + + static_assert(::sk_is_trivially_relocatable::value); + +#ifdef SK_DEBUG + SkString& validate(); + const SkString& validate() const; +#else + SkString& validate() { return *this; } + const SkString& validate() const { return *this; } +#endif + + static const Rec gEmptyRec; +}; + +/// Creates a new string and writes into it using a printf()-style format. +SK_API SkString SkStringPrintf(const char* format, ...) SK_PRINTF_LIKE(1, 2); +/// This makes it easier to write a caller as a VAR_ARGS function where the format string is +/// optional. +static inline SkString SkStringPrintf() { return SkString(); } + +static inline void swap(SkString& a, SkString& b) { + a.swap(b); +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkStrokeRec.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkStrokeRec.h new file mode 100644 index 00000000000..8617b25407d --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkStrokeRec.h @@ -0,0 +1,159 @@ +/* + * Copyright 2012 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkStrokeRec_DEFINED +#define SkStrokeRec_DEFINED + +#include "include/core/SkPaint.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkMacros.h" + +#include +#include + +class SkPath; + +SK_BEGIN_REQUIRE_DENSE +class SK_API SkStrokeRec { +public: + enum InitStyle { + kHairline_InitStyle, + kFill_InitStyle + }; + SkStrokeRec(InitStyle style); + SkStrokeRec(const SkPaint&, SkPaint::Style, SkScalar resScale = 1); + explicit SkStrokeRec(const SkPaint&, SkScalar resScale = 1); + + enum Style { + kHairline_Style, + kFill_Style, + kStroke_Style, + kStrokeAndFill_Style + }; + + static constexpr int kStyleCount = kStrokeAndFill_Style + 1; + + Style getStyle() const; + SkScalar getWidth() const { return fWidth; } + SkScalar getMiter() const { return fMiterLimit; } + SkPaint::Cap getCap() const { return (SkPaint::Cap)fCap; } + SkPaint::Join getJoin() const { return (SkPaint::Join)fJoin; } + + bool isHairlineStyle() const { + return kHairline_Style == this->getStyle(); + } + + bool isFillStyle() const { + return kFill_Style == this->getStyle(); + } + + void setFillStyle(); + void setHairlineStyle(); + /** + * Specify the strokewidth, and optionally if you want stroke + fill. + * Note, if width==0, then this request is taken to mean: + * strokeAndFill==true -> new style will be Fill + * strokeAndFill==false -> new style will be Hairline + */ + void setStrokeStyle(SkScalar width, bool strokeAndFill = false); + + void setStrokeParams(SkPaint::Cap cap, SkPaint::Join join, SkScalar miterLimit) { + fCap = cap; + fJoin = join; + fMiterLimit = miterLimit; + } + + SkScalar getResScale() const { + return fResScale; + } + + void setResScale(SkScalar rs) { + SkASSERT(rs > 0 && std::isfinite(rs)); + fResScale = rs; + } + + /** + * Returns true if this specifes any thick stroking, i.e. applyToPath() + * will return true. + */ + bool needToApply() const { + Style style = this->getStyle(); + return (kStroke_Style == style) || (kStrokeAndFill_Style == style); + } + + /** + * Apply these stroke parameters to the src path, returning the result + * in dst. + * + * If there was no change (i.e. style == hairline or fill) this returns + * false and dst is unchanged. Otherwise returns true and the result is + * stored in dst. + * + * src and dst may be the same path. + */ + bool applyToPath(SkPath* dst, const SkPath& src) const; + + /** + * Apply these stroke parameters to a paint. + */ + void applyToPaint(SkPaint* paint) const; + + /** + * Gives a conservative value for the outset that should applied to a + * geometries bounds to account for any inflation due to applying this + * strokeRec to the geometry. + */ + SkScalar getInflationRadius() const; + + /** + * Equivalent to: + * SkStrokeRec rec(paint, style); + * rec.getInflationRadius(); + * This does not account for other effects on the paint (i.e. path + * effect). + */ + static SkScalar GetInflationRadius(const SkPaint&, SkPaint::Style); + + static SkScalar GetInflationRadius(SkPaint::Join, SkScalar miterLimit, SkPaint::Cap, + SkScalar strokeWidth); + + /** + * Compare if two SkStrokeRecs have an equal effect on a path. + * Equal SkStrokeRecs produce equal paths. Equality of produced + * paths does not take the ResScale parameter into account. + */ + bool hasEqualEffect(const SkStrokeRec& other) const { + if (!this->needToApply()) { + return this->getStyle() == other.getStyle(); + } + return fWidth == other.fWidth && + (fJoin != SkPaint::kMiter_Join || fMiterLimit == other.fMiterLimit) && + fCap == other.fCap && + fJoin == other.fJoin && + fStrokeAndFill == other.fStrokeAndFill; + } + +private: + void init(const SkPaint&, SkPaint::Style, SkScalar resScale); + + SkScalar fResScale; + SkScalar fWidth; + SkScalar fMiterLimit; + // The following three members are packed together into a single u32. + // This is to avoid unnecessary padding and ensure binary equality for + // hashing (because the padded areas might contain garbage values). + // + // fCap and fJoin are larger than needed to avoid having to initialize + // any pad values + uint32_t fCap : 16; // SkPaint::Cap + uint32_t fJoin : 15; // SkPaint::Join + uint32_t fStrokeAndFill : 1; // bool +}; +SK_END_REQUIRE_DENSE + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSurface.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSurface.h new file mode 100644 index 00000000000..72f1bc2bf06 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSurface.h @@ -0,0 +1,659 @@ +/* + * Copyright 2012 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkSurface_DEFINED +#define SkSurface_DEFINED + +#include "include/core/SkImage.h" +#include "include/core/SkImageInfo.h" +#include "include/core/SkPixmap.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkSamplingOptions.h" +#include "include/core/SkScalar.h" +#include "include/core/SkSurfaceProps.h" +#include "include/core/SkTypes.h" + +#include +#include +#include + +class GrBackendSemaphore; +class GrBackendTexture; +class GrRecordingContext; +class GrSurfaceCharacterization; +enum GrSurfaceOrigin : int; +class SkBitmap; +class SkCanvas; +class SkCapabilities; +class SkColorSpace; +class SkPaint; +class SkSurface; +struct SkIRect; +struct SkISize; + +namespace skgpu::graphite { +class Recorder; +} + +namespace SkSurfaces { + +enum class BackendSurfaceAccess { + kNoAccess, //!< back-end surface will not be used by client + kPresent, //!< back-end surface will be used for presenting to screen +}; + +/** Returns SkSurface without backing pixels. Drawing to SkCanvas returned from SkSurface + has no effect. Calling makeImageSnapshot() on returned SkSurface returns nullptr. + + @param width one or greater + @param height one or greater + @return SkSurface if width and height are positive; otherwise, nullptr + + example: https://fiddle.skia.org/c/@Surface_MakeNull +*/ +SK_API sk_sp Null(int width, int height); + +/** Allocates raster SkSurface. SkCanvas returned by SkSurface draws directly into those allocated + pixels, which are zeroed before use. Pixel memory size is imageInfo.height() times + imageInfo.minRowBytes() or rowBytes, if provided and non-zero. + + Pixel memory is deleted when SkSurface is deleted. + + Validity constraints include: + - info dimensions are greater than zero; + - info contains SkColorType and SkAlphaType supported by raster surface. + + @param imageInfo width, height, SkColorType, SkAlphaType, SkColorSpace, + of raster surface; width and height must be greater than zero + @param rowBytes interval from one SkSurface row to the next. + @param props LCD striping orientation and setting for device independent fonts; + may be nullptr + @return SkSurface if parameters are valid and memory was allocated, else nullptr. +*/ +SK_API sk_sp Raster(const SkImageInfo& imageInfo, + size_t rowBytes, + const SkSurfaceProps* surfaceProps); +inline sk_sp Raster(const SkImageInfo& imageInfo, + const SkSurfaceProps* props = nullptr) { + return Raster(imageInfo, 0, props); +} + +/** Allocates raster SkSurface. SkCanvas returned by SkSurface draws directly into the + provided pixels. + + SkSurface is returned if all parameters are valid. + Valid parameters include: + info dimensions are greater than zero; + info contains SkColorType and SkAlphaType supported by raster surface; + pixels is not nullptr; + rowBytes is large enough to contain info width pixels of SkColorType. + + Pixel buffer size should be info height times computed rowBytes. + Pixels are not initialized. + To access pixels after drawing, peekPixels() or readPixels(). + + @param imageInfo width, height, SkColorType, SkAlphaType, SkColorSpace, + of raster surface; width and height must be greater than zero + @param pixels pointer to destination pixels buffer + @param rowBytes interval from one SkSurface row to the next + @param surfaceProps LCD striping orientation and setting for device independent fonts; + may be nullptr + @return SkSurface if all parameters are valid; otherwise, nullptr +*/ + +SK_API sk_sp WrapPixels(const SkImageInfo& imageInfo, + void* pixels, + size_t rowBytes, + const SkSurfaceProps* surfaceProps = nullptr); +inline sk_sp WrapPixels(const SkPixmap& pm, const SkSurfaceProps* props = nullptr) { + return WrapPixels(pm.info(), pm.writable_addr(), pm.rowBytes(), props); +} + +using PixelsReleaseProc = void(void* pixels, void* context); + +/** Allocates raster SkSurface. SkCanvas returned by SkSurface draws directly into the provided + pixels. releaseProc is called with pixels and context when SkSurface is deleted. + + SkSurface is returned if all parameters are valid. + Valid parameters include: + info dimensions are greater than zero; + info contains SkColorType and SkAlphaType supported by raster surface; + pixels is not nullptr; + rowBytes is large enough to contain info width pixels of SkColorType. + + Pixel buffer size should be info height times computed rowBytes. + Pixels are not initialized. + To access pixels after drawing, call flush() or peekPixels(). + + @param imageInfo width, height, SkColorType, SkAlphaType, SkColorSpace, + of raster surface; width and height must be greater than zero + @param pixels pointer to destination pixels buffer + @param rowBytes interval from one SkSurface row to the next + @param releaseProc called when SkSurface is deleted; may be nullptr + @param context passed to releaseProc; may be nullptr + @param surfaceProps LCD striping orientation and setting for device independent fonts; + may be nullptr + @return SkSurface if all parameters are valid; otherwise, nullptr +*/ +SK_API sk_sp WrapPixels(const SkImageInfo& imageInfo, + void* pixels, + size_t rowBytes, + PixelsReleaseProc, + void* context, + const SkSurfaceProps* surfaceProps = nullptr); +} // namespace SkSurfaces + +/** \class SkSurface + SkSurface is responsible for managing the pixels that a canvas draws into. The pixels can be + allocated either in CPU memory (a raster surface) or on the GPU (a GrRenderTarget surface). + SkSurface takes care of allocating a SkCanvas that will draw into the surface. Call + surface->getCanvas() to use that canvas (but don't delete it, it is owned by the surface). + SkSurface always has non-zero dimensions. If there is a request for a new surface, and either + of the requested dimensions are zero, then nullptr will be returned. + + Clients should *not* subclass SkSurface as there is a lot of internal machinery that is + not publicly accessible. +*/ +class SK_API SkSurface : public SkRefCnt { +public: + /** Is this surface compatible with the provided characterization? + + This method can be used to determine if an existing SkSurface is a viable destination + for an GrDeferredDisplayList. + + @param characterization The characterization for which a compatibility check is desired + @return true if this surface is compatible with the characterization; + false otherwise + */ + bool isCompatible(const GrSurfaceCharacterization& characterization) const; + + /** Returns pixel count in each row; may be zero or greater. + + @return number of pixel columns + */ + int width() const { return fWidth; } + + /** Returns pixel row count; may be zero or greater. + + @return number of pixel rows + */ + int height() const { return fHeight; } + + /** Returns an ImageInfo describing the surface. + */ + virtual SkImageInfo imageInfo() const { return SkImageInfo::MakeUnknown(fWidth, fHeight); } + + /** Returns unique value identifying the content of SkSurface. Returned value changes + each time the content changes. Content is changed by drawing, or by calling + notifyContentWillChange(). + + @return unique content identifier + + example: https://fiddle.skia.org/c/@Surface_notifyContentWillChange + */ + uint32_t generationID(); + + /** \enum SkSurface::ContentChangeMode + ContentChangeMode members are parameters to notifyContentWillChange(). + */ + enum ContentChangeMode { + kDiscard_ContentChangeMode, //!< discards surface on change + kRetain_ContentChangeMode, //!< preserves surface on change + }; + + /** Notifies that SkSurface contents will be changed by code outside of Skia. + Subsequent calls to generationID() return a different value. + + TODO: Can kRetain_ContentChangeMode be deprecated? + + example: https://fiddle.skia.org/c/@Surface_notifyContentWillChange + */ + void notifyContentWillChange(ContentChangeMode mode); + + /** Returns the recording context being used by the SkSurface. + + @return the recording context, if available; nullptr otherwise + */ + GrRecordingContext* recordingContext() const; + + /** Returns the recorder being used by the SkSurface. + + @return the recorder, if available; nullptr otherwise + */ + skgpu::graphite::Recorder* recorder() const; + + enum class BackendHandleAccess { + kFlushRead, //!< back-end object is readable + kFlushWrite, //!< back-end object is writable + kDiscardWrite, //!< back-end object must be overwritten + + // Legacy names, remove when clients are migrated + kFlushRead_BackendHandleAccess = kFlushRead, + kFlushWrite_BackendHandleAccess = kFlushWrite, + kDiscardWrite_BackendHandleAccess = kDiscardWrite, + }; + + // Legacy names, remove when clients are migrated + static constexpr BackendHandleAccess kFlushRead_BackendHandleAccess = + BackendHandleAccess::kFlushRead; + static constexpr BackendHandleAccess kFlushWrite_BackendHandleAccess = + BackendHandleAccess::kFlushWrite; + static constexpr BackendHandleAccess kDiscardWrite_BackendHandleAccess = + BackendHandleAccess::kDiscardWrite; + + /** Caller data passed to TextureReleaseProc; may be nullptr. */ + using ReleaseContext = void*; + /** User function called when supplied texture may be deleted. */ + using TextureReleaseProc = void (*)(ReleaseContext); + + /** If the surface was made via MakeFromBackendTexture then it's backing texture may be + substituted with a different texture. The contents of the previous backing texture are + copied into the new texture. SkCanvas state is preserved. The original sample count is + used. The GrBackendFormat and dimensions of replacement texture must match that of + the original. + + Upon success textureReleaseProc is called when it is safe to delete the texture in the + backend API (accounting only for use of the texture by this surface). If SkSurface creation + fails textureReleaseProc is called before this function returns. + + @param backendTexture the new backing texture for the surface + @param mode Retain or discard current Content + @param TextureReleaseProc function called when texture can be released + @param ReleaseContext state passed to textureReleaseProc + */ + virtual bool replaceBackendTexture(const GrBackendTexture& backendTexture, + GrSurfaceOrigin origin, + ContentChangeMode mode = kRetain_ContentChangeMode, + TextureReleaseProc = nullptr, + ReleaseContext = nullptr) = 0; + + /** Returns SkCanvas that draws into SkSurface. Subsequent calls return the same SkCanvas. + SkCanvas returned is managed and owned by SkSurface, and is deleted when SkSurface + is deleted. + + @return drawing SkCanvas for SkSurface + + example: https://fiddle.skia.org/c/@Surface_getCanvas + */ + SkCanvas* getCanvas(); + + /** Returns SkCapabilities that describes the capabilities of the SkSurface's device. + + @return SkCapabilities of SkSurface's device. + */ + sk_sp capabilities(); + + /** Returns a compatible SkSurface, or nullptr. Returned SkSurface contains + the same raster, GPU, or null properties as the original. Returned SkSurface + does not share the same pixels. + + Returns nullptr if imageInfo width or height are zero, or if imageInfo + is incompatible with SkSurface. + + @param imageInfo width, height, SkColorType, SkAlphaType, SkColorSpace, + of SkSurface; width and height must be greater than zero + @return compatible SkSurface or nullptr + + example: https://fiddle.skia.org/c/@Surface_makeSurface + */ + sk_sp makeSurface(const SkImageInfo& imageInfo); + + /** Calls makeSurface(ImageInfo) with the same ImageInfo as this surface, but with the + * specified width and height. + */ + sk_sp makeSurface(int width, int height); + + /** Returns SkImage capturing SkSurface contents. Subsequent drawing to SkSurface contents + are not captured. SkImage allocation is accounted for if SkSurface was created with + skgpu::Budgeted::kYes. + + @return SkImage initialized with SkSurface contents + + example: https://fiddle.skia.org/c/@Surface_makeImageSnapshot + */ + sk_sp makeImageSnapshot(); + + /** + * Like the no-parameter version, this returns an image of the current surface contents. + * This variant takes a rectangle specifying the subset of the surface that is of interest. + * These bounds will be sanitized before being used. + * - If bounds extends beyond the surface, it will be trimmed to just the intersection of + * it and the surface. + * - If bounds does not intersect the surface, then this returns nullptr. + * - If bounds == the surface, then this is the same as calling the no-parameter variant. + + example: https://fiddle.skia.org/c/@Surface_makeImageSnapshot_2 + */ + sk_sp makeImageSnapshot(const SkIRect& bounds); + + /** Draws SkSurface contents to canvas, with its top-left corner at (x, y). + + If SkPaint paint is not nullptr, apply SkColorFilter, alpha, SkImageFilter, and SkBlendMode. + + @param canvas SkCanvas drawn into + @param x horizontal offset in SkCanvas + @param y vertical offset in SkCanvas + @param sampling what technique to use when sampling the surface pixels + @param paint SkPaint containing SkBlendMode, SkColorFilter, SkImageFilter, + and so on; or nullptr + + example: https://fiddle.skia.org/c/@Surface_draw + */ + void draw(SkCanvas* canvas, SkScalar x, SkScalar y, const SkSamplingOptions& sampling, + const SkPaint* paint); + + void draw(SkCanvas* canvas, SkScalar x, SkScalar y, const SkPaint* paint = nullptr) { + this->draw(canvas, x, y, SkSamplingOptions(), paint); + } + + /** Copies SkSurface pixel address, row bytes, and SkImageInfo to SkPixmap, if address + is available, and returns true. If pixel address is not available, return + false and leave SkPixmap unchanged. + + pixmap contents become invalid on any future change to SkSurface. + + @param pixmap storage for pixel state if pixels are readable; otherwise, ignored + @return true if SkSurface has direct access to pixels + + example: https://fiddle.skia.org/c/@Surface_peekPixels + */ + bool peekPixels(SkPixmap* pixmap); + + /** Copies SkRect of pixels to dst. + + Source SkRect corners are (srcX, srcY) and SkSurface (width(), height()). + Destination SkRect corners are (0, 0) and (dst.width(), dst.height()). + Copies each readable pixel intersecting both rectangles, without scaling, + converting to dst.colorType() and dst.alphaType() if required. + + Pixels are readable when SkSurface is raster, or backed by a GPU. + + The destination pixel storage must be allocated by the caller. + + Pixel values are converted only if SkColorType and SkAlphaType + do not match. Only pixels within both source and destination rectangles + are copied. dst contents outside SkRect intersection are unchanged. + + Pass negative values for srcX or srcY to offset pixels across or down destination. + + Does not copy, and returns false if: + - Source and destination rectangles do not intersect. + - SkPixmap pixels could not be allocated. + - dst.rowBytes() is too small to contain one row of pixels. + + @param dst storage for pixels copied from SkSurface + @param srcX offset into readable pixels on x-axis; may be negative + @param srcY offset into readable pixels on y-axis; may be negative + @return true if pixels were copied + + example: https://fiddle.skia.org/c/@Surface_readPixels + */ + bool readPixels(const SkPixmap& dst, int srcX, int srcY); + + /** Copies SkRect of pixels from SkCanvas into dstPixels. + + Source SkRect corners are (srcX, srcY) and SkSurface (width(), height()). + Destination SkRect corners are (0, 0) and (dstInfo.width(), dstInfo.height()). + Copies each readable pixel intersecting both rectangles, without scaling, + converting to dstInfo.colorType() and dstInfo.alphaType() if required. + + Pixels are readable when SkSurface is raster, or backed by a GPU. + + The destination pixel storage must be allocated by the caller. + + Pixel values are converted only if SkColorType and SkAlphaType + do not match. Only pixels within both source and destination rectangles + are copied. dstPixels contents outside SkRect intersection are unchanged. + + Pass negative values for srcX or srcY to offset pixels across or down destination. + + Does not copy, and returns false if: + - Source and destination rectangles do not intersect. + - SkSurface pixels could not be converted to dstInfo.colorType() or dstInfo.alphaType(). + - dstRowBytes is too small to contain one row of pixels. + + @param dstInfo width, height, SkColorType, and SkAlphaType of dstPixels + @param dstPixels storage for pixels; dstInfo.height() times dstRowBytes, or larger + @param dstRowBytes size of one destination row; dstInfo.width() times pixel size, or larger + @param srcX offset into readable pixels on x-axis; may be negative + @param srcY offset into readable pixels on y-axis; may be negative + @return true if pixels were copied + */ + bool readPixels(const SkImageInfo& dstInfo, void* dstPixels, size_t dstRowBytes, + int srcX, int srcY); + + /** Copies SkRect of pixels from SkSurface into bitmap. + + Source SkRect corners are (srcX, srcY) and SkSurface (width(), height()). + Destination SkRect corners are (0, 0) and (bitmap.width(), bitmap.height()). + Copies each readable pixel intersecting both rectangles, without scaling, + converting to bitmap.colorType() and bitmap.alphaType() if required. + + Pixels are readable when SkSurface is raster, or backed by a GPU. + + The destination pixel storage must be allocated by the caller. + + Pixel values are converted only if SkColorType and SkAlphaType + do not match. Only pixels within both source and destination rectangles + are copied. dst contents outside SkRect intersection are unchanged. + + Pass negative values for srcX or srcY to offset pixels across or down destination. + + Does not copy, and returns false if: + - Source and destination rectangles do not intersect. + - SkSurface pixels could not be converted to dst.colorType() or dst.alphaType(). + - dst pixels could not be allocated. + - dst.rowBytes() is too small to contain one row of pixels. + + @param dst storage for pixels copied from SkSurface + @param srcX offset into readable pixels on x-axis; may be negative + @param srcY offset into readable pixels on y-axis; may be negative + @return true if pixels were copied + + example: https://fiddle.skia.org/c/@Surface_readPixels_3 + */ + bool readPixels(const SkBitmap& dst, int srcX, int srcY); + + using AsyncReadResult = SkImage::AsyncReadResult; + + /** Client-provided context that is passed to client-provided ReadPixelsContext. */ + using ReadPixelsContext = void*; + + /** Client-provided callback to asyncRescaleAndReadPixels() or + asyncRescaleAndReadPixelsYUV420() that is called when read result is ready or on failure. + */ + using ReadPixelsCallback = void(ReadPixelsContext, std::unique_ptr); + + /** Controls the gamma that rescaling occurs in for asyncRescaleAndReadPixels() and + asyncRescaleAndReadPixelsYUV420(). + */ + using RescaleGamma = SkImage::RescaleGamma; + using RescaleMode = SkImage::RescaleMode; + + /** Makes surface pixel data available to caller, possibly asynchronously. It can also rescale + the surface pixels. + + Currently asynchronous reads are only supported on the GPU backend and only when the + underlying 3D API supports transfer buffers and CPU/GPU synchronization primitives. In all + other cases this operates synchronously. + + Data is read from the source sub-rectangle, is optionally converted to a linear gamma, is + rescaled to the size indicated by 'info', is then converted to the color space, color type, + and alpha type of 'info'. A 'srcRect' that is not contained by the bounds of the surface + causes failure. + + When the pixel data is ready the caller's ReadPixelsCallback is called with a + AsyncReadResult containing pixel data in the requested color type, alpha type, and color + space. The AsyncReadResult will have count() == 1. Upon failure the callback is called + with nullptr for AsyncReadResult. For a GPU surface this flushes work but a submit must + occur to guarantee a finite time before the callback is called. + + The data is valid for the lifetime of AsyncReadResult with the exception that if the + SkSurface is GPU-backed the data is immediately invalidated if the context is abandoned + or destroyed. + + @param info info of the requested pixels + @param srcRect subrectangle of surface to read + @param rescaleGamma controls whether rescaling is done in the surface's gamma or whether + the source data is transformed to a linear gamma before rescaling. + @param rescaleMode controls the technique of the rescaling + @param callback function to call with result of the read + @param context passed to callback + */ + void asyncRescaleAndReadPixels(const SkImageInfo& info, + const SkIRect& srcRect, + RescaleGamma rescaleGamma, + RescaleMode rescaleMode, + ReadPixelsCallback callback, + ReadPixelsContext context); + + /** + Similar to asyncRescaleAndReadPixels but performs an additional conversion to YUV. The + RGB->YUV conversion is controlled by 'yuvColorSpace'. The YUV data is returned as three + planes ordered y, u, v. The u and v planes are half the width and height of the resized + rectangle. The y, u, and v values are single bytes. Currently this fails if 'dstSize' + width and height are not even. A 'srcRect' that is not contained by the bounds of the + surface causes failure. + + When the pixel data is ready the caller's ReadPixelsCallback is called with a + AsyncReadResult containing the planar data. The AsyncReadResult will have count() == 3. + Upon failure the callback is called with nullptr for AsyncReadResult. For a GPU surface this + flushes work but a submit must occur to guarantee a finite time before the callback is + called. + + The data is valid for the lifetime of AsyncReadResult with the exception that if the + SkSurface is GPU-backed the data is immediately invalidated if the context is abandoned + or destroyed. + + @param yuvColorSpace The transformation from RGB to YUV. Applied to the resized image + after it is converted to dstColorSpace. + @param dstColorSpace The color space to convert the resized image to, after rescaling. + @param srcRect The portion of the surface to rescale and convert to YUV planes. + @param dstSize The size to rescale srcRect to + @param rescaleGamma controls whether rescaling is done in the surface's gamma or whether + the source data is transformed to a linear gamma before rescaling. + @param rescaleMode controls the sampling technique of the rescaling + @param callback function to call with the planar read result + @param context passed to callback + */ + void asyncRescaleAndReadPixelsYUV420(SkYUVColorSpace yuvColorSpace, + sk_sp dstColorSpace, + const SkIRect& srcRect, + const SkISize& dstSize, + RescaleGamma rescaleGamma, + RescaleMode rescaleMode, + ReadPixelsCallback callback, + ReadPixelsContext context); + + /** + * Identical to asyncRescaleAndReadPixelsYUV420 but a fourth plane is returned in the + * AsyncReadResult passed to 'callback'. The fourth plane contains the alpha chanel at the + * same full resolution as the Y plane. + */ + void asyncRescaleAndReadPixelsYUVA420(SkYUVColorSpace yuvColorSpace, + sk_sp dstColorSpace, + const SkIRect& srcRect, + const SkISize& dstSize, + RescaleGamma rescaleGamma, + RescaleMode rescaleMode, + ReadPixelsCallback callback, + ReadPixelsContext context); + + /** Copies SkRect of pixels from the src SkPixmap to the SkSurface. + + Source SkRect corners are (0, 0) and (src.width(), src.height()). + Destination SkRect corners are (dstX, dstY) and + (dstX + Surface width(), dstY + Surface height()). + + Copies each readable pixel intersecting both rectangles, without scaling, + converting to SkSurface colorType() and SkSurface alphaType() if required. + + @param src storage for pixels to copy to SkSurface + @param dstX x-axis position relative to SkSurface to begin copy; may be negative + @param dstY y-axis position relative to SkSurface to begin copy; may be negative + + example: https://fiddle.skia.org/c/@Surface_writePixels + */ + void writePixels(const SkPixmap& src, int dstX, int dstY); + + /** Copies SkRect of pixels from the src SkBitmap to the SkSurface. + + Source SkRect corners are (0, 0) and (src.width(), src.height()). + Destination SkRect corners are (dstX, dstY) and + (dstX + Surface width(), dstY + Surface height()). + + Copies each readable pixel intersecting both rectangles, without scaling, + converting to SkSurface colorType() and SkSurface alphaType() if required. + + @param src storage for pixels to copy to SkSurface + @param dstX x-axis position relative to SkSurface to begin copy; may be negative + @param dstY y-axis position relative to SkSurface to begin copy; may be negative + + example: https://fiddle.skia.org/c/@Surface_writePixels_2 + */ + void writePixels(const SkBitmap& src, int dstX, int dstY); + + /** Returns SkSurfaceProps for surface. + + @return LCD striping orientation and setting for device independent fonts + */ + const SkSurfaceProps& props() const { return fProps; } + + /** Inserts a list of GPU semaphores that the current GPU-backed API must wait on before + executing any more commands on the GPU for this surface. We only guarantee blocking + transfer and fragment shader work, but may block earlier stages as well depending on the + backend. + If this call returns false, then the GPU back-end will not wait on any passed in + semaphores, and the client will still own the semaphores, regardless of the value of + deleteSemaphoresAfterWait. + + If deleteSemaphoresAfterWait is false then Skia will not delete the semaphores. In this case + it is the client's responsibility to not destroy or attempt to reuse the semaphores until it + knows that Skia has finished waiting on them. This can be done by using finishedProcs + on flush calls. + + @param numSemaphores size of waitSemaphores array + @param waitSemaphores array of semaphore containers + @paramm deleteSemaphoresAfterWait who owns and should delete the semaphores + @return true if GPU is waiting on semaphores + */ + bool wait(int numSemaphores, const GrBackendSemaphore* waitSemaphores, + bool deleteSemaphoresAfterWait = true); + + /** Initializes GrSurfaceCharacterization that can be used to perform GPU back-end + processing in a separate thread. Typically this is used to divide drawing + into multiple tiles. GrDeferredDisplayListRecorder records the drawing commands + for each tile. + + Return true if SkSurface supports characterization. raster surface returns false. + + @param characterization properties for parallel drawing + @return true if supported + + example: https://fiddle.skia.org/c/@Surface_characterize + */ + bool characterize(GrSurfaceCharacterization* characterization) const; + +protected: + SkSurface(int width, int height, const SkSurfaceProps* surfaceProps); + SkSurface(const SkImageInfo& imageInfo, const SkSurfaceProps* surfaceProps); + + // called by subclass if their contents have changed + void dirtyGenerationID() { + fGenerationID = 0; + } + +private: + const SkSurfaceProps fProps; + const int fWidth; + const int fHeight; + uint32_t fGenerationID; + + using INHERITED = SkRefCnt; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSurfaceProps.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSurfaceProps.h new file mode 100644 index 00000000000..d8509bf53e1 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSurfaceProps.h @@ -0,0 +1,116 @@ +/* + * Copyright 2014 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkSurfaceProps_DEFINED +#define SkSurfaceProps_DEFINED + +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkTo.h" + +/** + * Description of how the LCD strips are arranged for each pixel. If this is unknown, or the + * pixels are meant to be "portable" and/or transformed before showing (e.g. rotated, scaled) + * then use kUnknown_SkPixelGeometry. + */ +enum SkPixelGeometry { + kUnknown_SkPixelGeometry, + kRGB_H_SkPixelGeometry, + kBGR_H_SkPixelGeometry, + kRGB_V_SkPixelGeometry, + kBGR_V_SkPixelGeometry, +}; + +// Returns true iff geo is a known geometry and is RGB. +static inline bool SkPixelGeometryIsRGB(SkPixelGeometry geo) { + return kRGB_H_SkPixelGeometry == geo || kRGB_V_SkPixelGeometry == geo; +} + +// Returns true iff geo is a known geometry and is BGR. +static inline bool SkPixelGeometryIsBGR(SkPixelGeometry geo) { + return kBGR_H_SkPixelGeometry == geo || kBGR_V_SkPixelGeometry == geo; +} + +// Returns true iff geo is a known geometry and is horizontal. +static inline bool SkPixelGeometryIsH(SkPixelGeometry geo) { + return kRGB_H_SkPixelGeometry == geo || kBGR_H_SkPixelGeometry == geo; +} + +// Returns true iff geo is a known geometry and is vertical. +static inline bool SkPixelGeometryIsV(SkPixelGeometry geo) { + return kRGB_V_SkPixelGeometry == geo || kBGR_V_SkPixelGeometry == geo; +} + +/** + * Describes properties and constraints of a given SkSurface. The rendering engine can parse these + * during drawing, and can sometimes optimize its performance (e.g. disabling an expensive + * feature). + */ +class SK_API SkSurfaceProps { +public: + enum Flags { + kDefault_Flag = 0, + kUseDeviceIndependentFonts_Flag = 1 << 0, + // Use internal MSAA to render to non-MSAA GPU surfaces. + kDynamicMSAA_Flag = 1 << 1, + // If set, all rendering will have dithering enabled + // Currently this only impacts GPU backends + kAlwaysDither_Flag = 1 << 2, + }; + + /** No flags, unknown pixel geometry, platform-default contrast/gamma. */ + SkSurfaceProps(); + /** TODO(kschmi): Remove this constructor and replace with the one below. **/ + SkSurfaceProps(uint32_t flags, SkPixelGeometry); + /** Specified pixel geometry, text contrast, and gamma **/ + SkSurfaceProps(uint32_t flags, SkPixelGeometry, SkScalar textContrast, SkScalar textGamma); + + SkSurfaceProps(const SkSurfaceProps&) = default; + SkSurfaceProps& operator=(const SkSurfaceProps&) = default; + + SkSurfaceProps cloneWithPixelGeometry(SkPixelGeometry newPixelGeometry) const { + return SkSurfaceProps(fFlags, newPixelGeometry, fTextContrast, fTextGamma); + } + + static constexpr SkScalar kMaxContrastInclusive = 1; + static constexpr SkScalar kMinContrastInclusive = 0; + static constexpr SkScalar kMaxGammaExclusive = 4; + static constexpr SkScalar kMinGammaInclusive = 0; + + uint32_t flags() const { return fFlags; } + SkPixelGeometry pixelGeometry() const { return fPixelGeometry; } + SkScalar textContrast() const { return fTextContrast; } + SkScalar textGamma() const { return fTextGamma; } + + bool isUseDeviceIndependentFonts() const { + return SkToBool(fFlags & kUseDeviceIndependentFonts_Flag); + } + + bool isAlwaysDither() const { + return SkToBool(fFlags & kAlwaysDither_Flag); + } + + bool operator==(const SkSurfaceProps& that) const { + return fFlags == that.fFlags && fPixelGeometry == that.fPixelGeometry && + fTextContrast == that.fTextContrast && fTextGamma == that.fTextGamma; + } + + bool operator!=(const SkSurfaceProps& that) const { + return !(*this == that); + } + +private: + uint32_t fFlags; + SkPixelGeometry fPixelGeometry; + + // This gamma value is specifically about blending of mask coverage. + // The surface also has a color space, but that applies to the colors. + SkScalar fTextContrast; + SkScalar fTextGamma; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSwizzle.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSwizzle.h new file mode 100644 index 00000000000..f7ef6588928 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkSwizzle.h @@ -0,0 +1,21 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkSwizzle_DEFINED +#define SkSwizzle_DEFINED + +#include "include/private/base/SkAPI.h" + +#include + +/** + Swizzles byte order of |count| 32-bit pixels, swapping R and B. + (RGBA <-> BGRA) +*/ +SK_API void SkSwapRB(uint32_t* dest, const uint32_t* src, int count); + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTextBlob.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTextBlob.h new file mode 100644 index 00000000000..23e2d9bf2a6 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTextBlob.h @@ -0,0 +1,519 @@ +/* + * Copyright 2014 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTextBlob_DEFINED +#define SkTextBlob_DEFINED + +#include "include/core/SkFont.h" +#include "include/core/SkFontTypes.h" +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkDebug.h" +#include "include/private/base/SkTemplates.h" + +#include +#include +#include + +class SkData; +class SkPaint; +class SkTypeface; +struct SkDeserialProcs; +struct SkPoint; +struct SkRSXform; +struct SkSerialProcs; + +namespace sktext { +class GlyphRunList; +} + +/** \class SkTextBlob + SkTextBlob combines multiple text runs into an immutable container. Each text + run consists of glyphs, SkPaint, and position. Only parts of SkPaint related to + fonts and text rendering are used by run. +*/ +class SK_API SkTextBlob final : public SkNVRefCnt { +private: + class RunRecord; + +public: + + /** Returns conservative bounding box. Uses SkPaint associated with each glyph to + determine glyph bounds, and unions all bounds. Returned bounds may be + larger than the bounds of all glyphs in runs. + + @return conservative bounding box + */ + const SkRect& bounds() const { return fBounds; } + + /** Returns a non-zero value unique among all text blobs. + + @return identifier for SkTextBlob + */ + uint32_t uniqueID() const { return fUniqueID; } + + /** Returns the number of intervals that intersect bounds. + bounds describes a pair of lines parallel to the text advance. + The return count is zero or a multiple of two, and is at most twice the number of glyphs in + the the blob. + + Pass nullptr for intervals to determine the size of the interval array. + + Runs within the blob that contain SkRSXform are ignored when computing intercepts. + + @param bounds lower and upper line parallel to the advance + @param intervals returned intersections; may be nullptr + @param paint specifies stroking, SkPathEffect that affects the result; may be nullptr + @return number of intersections; may be zero + */ + int getIntercepts(const SkScalar bounds[2], SkScalar intervals[], + const SkPaint* paint = nullptr) const; + + /** Creates SkTextBlob with a single run. + + font contains attributes used to define the run text. + + When encoding is SkTextEncoding::kUTF8, SkTextEncoding::kUTF16, or + SkTextEncoding::kUTF32, this function uses the default + character-to-glyph mapping from the SkTypeface in font. It does not + perform typeface fallback for characters not found in the SkTypeface. + It does not perform kerning or other complex shaping; glyphs are + positioned based on their default advances. + + @param text character code points or glyphs drawn + @param byteLength byte length of text array + @param font text size, typeface, text scale, and so on, used to draw + @param encoding text encoding used in the text array + @return SkTextBlob constructed from one run + */ + static sk_sp MakeFromText(const void* text, size_t byteLength, const SkFont& font, + SkTextEncoding encoding = SkTextEncoding::kUTF8); + + /** Creates SkTextBlob with a single run. string meaning depends on SkTextEncoding; + by default, string is encoded as UTF-8. + + font contains attributes used to define the run text. + + When encoding is SkTextEncoding::kUTF8, SkTextEncoding::kUTF16, or + SkTextEncoding::kUTF32, this function uses the default + character-to-glyph mapping from the SkTypeface in font. It does not + perform typeface fallback for characters not found in the SkTypeface. + It does not perform kerning or other complex shaping; glyphs are + positioned based on their default advances. + + @param string character code points or glyphs drawn + @param font text size, typeface, text scale, and so on, used to draw + @param encoding text encoding used in the text array + @return SkTextBlob constructed from one run + */ + static sk_sp MakeFromString(const char* string, const SkFont& font, + SkTextEncoding encoding = SkTextEncoding::kUTF8) { + if (!string) { + return nullptr; + } + return MakeFromText(string, strlen(string), font, encoding); + } + + /** Returns a textblob built from a single run of text with x-positions and a single y value. + This is equivalent to using SkTextBlobBuilder and calling allocRunPosH(). + Returns nullptr if byteLength is zero. + + @param text character code points or glyphs drawn (based on encoding) + @param byteLength byte length of text array + @param xpos array of x-positions, must contain values for all of the character points. + @param constY shared y-position for each character point, to be paired with each xpos. + @param font SkFont used for this run + @param encoding specifies the encoding of the text array. + @return new textblob or nullptr + */ + static sk_sp MakeFromPosTextH(const void* text, size_t byteLength, + const SkScalar xpos[], SkScalar constY, const SkFont& font, + SkTextEncoding encoding = SkTextEncoding::kUTF8); + + /** Returns a textblob built from a single run of text with positions. + This is equivalent to using SkTextBlobBuilder and calling allocRunPos(). + Returns nullptr if byteLength is zero. + + @param text character code points or glyphs drawn (based on encoding) + @param byteLength byte length of text array + @param pos array of positions, must contain values for all of the character points. + @param font SkFont used for this run + @param encoding specifies the encoding of the text array. + @return new textblob or nullptr + */ + static sk_sp MakeFromPosText(const void* text, size_t byteLength, + const SkPoint pos[], const SkFont& font, + SkTextEncoding encoding = SkTextEncoding::kUTF8); + + static sk_sp MakeFromRSXform(const void* text, size_t byteLength, + const SkRSXform xform[], const SkFont& font, + SkTextEncoding encoding = SkTextEncoding::kUTF8); + + /** Writes data to allow later reconstruction of SkTextBlob. memory points to storage + to receive the encoded data, and memory_size describes the size of storage. + Returns bytes used if provided storage is large enough to hold all data; + otherwise, returns zero. + + procs.fTypefaceProc permits supplying a custom function to encode SkTypeface. + If procs.fTypefaceProc is nullptr, default encoding is used. procs.fTypefaceCtx + may be used to provide user context to procs.fTypefaceProc; procs.fTypefaceProc + is called with a pointer to SkTypeface and user context. + + @param procs custom serial data encoders; may be nullptr + @param memory storage for data + @param memory_size size of storage + @return bytes written, or zero if required storage is larger than memory_size + + example: https://fiddle.skia.org/c/@TextBlob_serialize + */ + size_t serialize(const SkSerialProcs& procs, void* memory, size_t memory_size) const; + + /** Returns storage containing SkData describing SkTextBlob, using optional custom + encoders. + + procs.fTypefaceProc permits supplying a custom function to encode SkTypeface. + If procs.fTypefaceProc is nullptr, default encoding is used. procs.fTypefaceCtx + may be used to provide user context to procs.fTypefaceProc; procs.fTypefaceProc + is called with a pointer to SkTypeface and user context. + + @param procs custom serial data encoders; may be nullptr + @return storage containing serialized SkTextBlob + + example: https://fiddle.skia.org/c/@TextBlob_serialize_2 + */ + sk_sp serialize(const SkSerialProcs& procs) const; + + /** Recreates SkTextBlob that was serialized into data. Returns constructed SkTextBlob + if successful; otherwise, returns nullptr. Fails if size is smaller than + required data length, or if data does not permit constructing valid SkTextBlob. + + procs.fTypefaceProc permits supplying a custom function to decode SkTypeface. + If procs.fTypefaceProc is nullptr, default decoding is used. procs.fTypefaceCtx + may be used to provide user context to procs.fTypefaceProc; procs.fTypefaceProc + is called with a pointer to SkTypeface data, data byte length, and user context. + + @param data pointer for serial data + @param size size of data + @param procs custom serial data decoders; may be nullptr + @return SkTextBlob constructed from data in memory + */ + static sk_sp Deserialize(const void* data, size_t size, + const SkDeserialProcs& procs); + + class SK_API Iter { + public: + struct Run { + SkTypeface* fTypeface; + int fGlyphCount; + const uint16_t* fGlyphIndices; +#ifdef SK_UNTIL_CRBUG_1187654_IS_FIXED + const uint32_t* fClusterIndex_forTest; + int fUtf8Size_forTest; + const char* fUtf8_forTest; +#endif + }; + + Iter(const SkTextBlob&); + + /** + * Returns true for each "run" inside the textblob, setting the Run fields (if not null). + * If this returns false, there are no more runs, and the Run parameter will be ignored. + */ + bool next(Run*); + + // Experimental, DO NO USE, will change/go-away + struct ExperimentalRun { + SkFont font; + int count; + const uint16_t* glyphs; + const SkPoint* positions; + }; + bool experimentalNext(ExperimentalRun*); + + private: + const RunRecord* fRunRecord; + }; + +private: + friend class SkNVRefCnt; + + enum GlyphPositioning : uint8_t; + + explicit SkTextBlob(const SkRect& bounds); + + ~SkTextBlob(); + + // Memory for objects of this class is created with sk_malloc rather than operator new and must + // be freed with sk_free. + void operator delete(void* p); + void* operator new(size_t); + void* operator new(size_t, void* p); + + static unsigned ScalarsPerGlyph(GlyphPositioning pos); + + using PurgeDelegate = void (*)(uint32_t blobID, uint32_t cacheID); + + // Call when this blob is part of the key to a cache entry. This allows the cache + // to know automatically those entries can be purged when this SkTextBlob is deleted. + void notifyAddedToCache(uint32_t cacheID, PurgeDelegate purgeDelegate) const { + fCacheID.store(cacheID); + fPurgeDelegate.store(purgeDelegate); + } + + friend class sktext::GlyphRunList; + friend class SkTextBlobBuilder; + friend class SkTextBlobPriv; + friend class SkTextBlobRunIterator; + + const SkRect fBounds; + const uint32_t fUniqueID; + mutable std::atomic fCacheID; + mutable std::atomic fPurgeDelegate; + + SkDEBUGCODE(size_t fStorageSize;) + + // The actual payload resides in externally-managed storage, following the object. + // (see the .cpp for more details) + + using INHERITED = SkRefCnt; +}; + +/** \class SkTextBlobBuilder + Helper class for constructing SkTextBlob. +*/ +class SK_API SkTextBlobBuilder { +public: + + /** Constructs empty SkTextBlobBuilder. By default, SkTextBlobBuilder has no runs. + + @return empty SkTextBlobBuilder + + example: https://fiddle.skia.org/c/@TextBlobBuilder_empty_constructor + */ + SkTextBlobBuilder(); + + /** Deletes data allocated internally by SkTextBlobBuilder. + */ + ~SkTextBlobBuilder(); + + /** Returns SkTextBlob built from runs of glyphs added by builder. Returned + SkTextBlob is immutable; it may be copied, but its contents may not be altered. + Returns nullptr if no runs of glyphs were added by builder. + + Resets SkTextBlobBuilder to its initial empty state, allowing it to be + reused to build a new set of runs. + + @return SkTextBlob or nullptr + + example: https://fiddle.skia.org/c/@TextBlobBuilder_make + */ + sk_sp make(); + + /** \struct SkTextBlobBuilder::RunBuffer + RunBuffer supplies storage for glyphs and positions within a run. + + A run is a sequence of glyphs sharing font metrics and positioning. + Each run may position its glyphs in one of three ways: + by specifying where the first glyph is drawn, and allowing font metrics to + determine the advance to subsequent glyphs; by specifying a baseline, and + the position on that baseline for each glyph in run; or by providing SkPoint + array, one per glyph. + */ + struct RunBuffer { + SkGlyphID* glyphs; //!< storage for glyph indexes in run + SkScalar* pos; //!< storage for glyph positions in run + char* utf8text; //!< storage for text UTF-8 code units in run + uint32_t* clusters; //!< storage for glyph clusters (index of UTF-8 code unit) + + // Helpers, since the "pos" field can be different types (always some number of floats). + SkPoint* points() const { return reinterpret_cast(pos); } + SkRSXform* xforms() const { return reinterpret_cast(pos); } + }; + + /** Returns run with storage for glyphs. Caller must write count glyphs to + RunBuffer::glyphs before next call to SkTextBlobBuilder. + + RunBuffer::pos, RunBuffer::utf8text, and RunBuffer::clusters should be ignored. + + Glyphs share metrics in font. + + Glyphs are positioned on a baseline at (x, y), using font metrics to + determine their relative placement. + + bounds defines an optional bounding box, used to suppress drawing when SkTextBlob + bounds does not intersect SkSurface bounds. If bounds is nullptr, SkTextBlob bounds + is computed from (x, y) and RunBuffer::glyphs metrics. + + @param font SkFont used for this run + @param count number of glyphs + @param x horizontal offset within the blob + @param y vertical offset within the blob + @param bounds optional run bounding box + @return writable glyph buffer + */ + const RunBuffer& allocRun(const SkFont& font, int count, SkScalar x, SkScalar y, + const SkRect* bounds = nullptr); + + /** Returns run with storage for glyphs and positions along baseline. Caller must + write count glyphs to RunBuffer::glyphs and count scalars to RunBuffer::pos + before next call to SkTextBlobBuilder. + + RunBuffer::utf8text and RunBuffer::clusters should be ignored. + + Glyphs share metrics in font. + + Glyphs are positioned on a baseline at y, using x-axis positions written by + caller to RunBuffer::pos. + + bounds defines an optional bounding box, used to suppress drawing when SkTextBlob + bounds does not intersect SkSurface bounds. If bounds is nullptr, SkTextBlob bounds + is computed from y, RunBuffer::pos, and RunBuffer::glyphs metrics. + + @param font SkFont used for this run + @param count number of glyphs + @param y vertical offset within the blob + @param bounds optional run bounding box + @return writable glyph buffer and x-axis position buffer + */ + const RunBuffer& allocRunPosH(const SkFont& font, int count, SkScalar y, + const SkRect* bounds = nullptr); + + /** Returns run with storage for glyphs and SkPoint positions. Caller must + write count glyphs to RunBuffer::glyphs and count SkPoint to RunBuffer::pos + before next call to SkTextBlobBuilder. + + RunBuffer::utf8text and RunBuffer::clusters should be ignored. + + Glyphs share metrics in font. + + Glyphs are positioned using SkPoint written by caller to RunBuffer::pos, using + two scalar values for each SkPoint. + + bounds defines an optional bounding box, used to suppress drawing when SkTextBlob + bounds does not intersect SkSurface bounds. If bounds is nullptr, SkTextBlob bounds + is computed from RunBuffer::pos, and RunBuffer::glyphs metrics. + + @param font SkFont used for this run + @param count number of glyphs + @param bounds optional run bounding box + @return writable glyph buffer and SkPoint buffer + */ + const RunBuffer& allocRunPos(const SkFont& font, int count, + const SkRect* bounds = nullptr); + + // RunBuffer.pos points to SkRSXform array + const RunBuffer& allocRunRSXform(const SkFont& font, int count); + + /** Returns run with storage for glyphs, text, and clusters. Caller must + write count glyphs to RunBuffer::glyphs, textByteCount UTF-8 code units + into RunBuffer::utf8text, and count monotonic indexes into utf8text + into RunBuffer::clusters before next call to SkTextBlobBuilder. + + RunBuffer::pos should be ignored. + + Glyphs share metrics in font. + + Glyphs are positioned on a baseline at (x, y), using font metrics to + determine their relative placement. + + bounds defines an optional bounding box, used to suppress drawing when SkTextBlob + bounds does not intersect SkSurface bounds. If bounds is nullptr, SkTextBlob bounds + is computed from (x, y) and RunBuffer::glyphs metrics. + + @param font SkFont used for this run + @param count number of glyphs + @param x horizontal offset within the blob + @param y vertical offset within the blob + @param textByteCount number of UTF-8 code units + @param bounds optional run bounding box + @return writable glyph buffer, text buffer, and cluster buffer + */ + const RunBuffer& allocRunText(const SkFont& font, int count, SkScalar x, SkScalar y, + int textByteCount, const SkRect* bounds = nullptr); + + /** Returns run with storage for glyphs, positions along baseline, text, + and clusters. Caller must write count glyphs to RunBuffer::glyphs, + count scalars to RunBuffer::pos, textByteCount UTF-8 code units into + RunBuffer::utf8text, and count monotonic indexes into utf8text into + RunBuffer::clusters before next call to SkTextBlobBuilder. + + Glyphs share metrics in font. + + Glyphs are positioned on a baseline at y, using x-axis positions written by + caller to RunBuffer::pos. + + bounds defines an optional bounding box, used to suppress drawing when SkTextBlob + bounds does not intersect SkSurface bounds. If bounds is nullptr, SkTextBlob bounds + is computed from y, RunBuffer::pos, and RunBuffer::glyphs metrics. + + @param font SkFont used for this run + @param count number of glyphs + @param y vertical offset within the blob + @param textByteCount number of UTF-8 code units + @param bounds optional run bounding box + @return writable glyph buffer, x-axis position buffer, text buffer, and cluster buffer + */ + const RunBuffer& allocRunTextPosH(const SkFont& font, int count, SkScalar y, int textByteCount, + const SkRect* bounds = nullptr); + + /** Returns run with storage for glyphs, SkPoint positions, text, and + clusters. Caller must write count glyphs to RunBuffer::glyphs, count + SkPoint to RunBuffer::pos, textByteCount UTF-8 code units into + RunBuffer::utf8text, and count monotonic indexes into utf8text into + RunBuffer::clusters before next call to SkTextBlobBuilder. + + Glyphs share metrics in font. + + Glyphs are positioned using SkPoint written by caller to RunBuffer::pos, using + two scalar values for each SkPoint. + + bounds defines an optional bounding box, used to suppress drawing when SkTextBlob + bounds does not intersect SkSurface bounds. If bounds is nullptr, SkTextBlob bounds + is computed from RunBuffer::pos, and RunBuffer::glyphs metrics. + + @param font SkFont used for this run + @param count number of glyphs + @param textByteCount number of UTF-8 code units + @param bounds optional run bounding box + @return writable glyph buffer, SkPoint buffer, text buffer, and cluster buffer + */ + const RunBuffer& allocRunTextPos(const SkFont& font, int count, int textByteCount, + const SkRect* bounds = nullptr); + + // RunBuffer.pos points to SkRSXform array + const RunBuffer& allocRunTextRSXform(const SkFont& font, int count, int textByteCount, + const SkRect* bounds = nullptr); + +private: + void reserve(size_t size); + void allocInternal(const SkFont& font, SkTextBlob::GlyphPositioning positioning, + int count, int textBytes, SkPoint offset, const SkRect* bounds); + bool mergeRun(const SkFont& font, SkTextBlob::GlyphPositioning positioning, + uint32_t count, SkPoint offset); + void updateDeferredBounds(); + + static SkRect ConservativeRunBounds(const SkTextBlob::RunRecord&); + static SkRect TightRunBounds(const SkTextBlob::RunRecord&); + + friend class SkTextBlobPriv; + friend class SkTextBlobBuilderPriv; + + skia_private::AutoTMalloc fStorage; + size_t fStorageSize; + size_t fStorageUsed; + + SkRect fBounds; + int fRunCount; + bool fDeferredBounds; + size_t fLastRun; // index into fStorage + + RunBuffer fCurrentRunBuffer; +}; + +#endif // SkTextBlob_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTextureCompressionType.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTextureCompressionType.h new file mode 100644 index 00000000000..e9b441378d0 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTextureCompressionType.h @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTextureCompressionType_DEFINED +#define SkTextureCompressionType_DEFINED +/* + * Skia | GL_COMPRESSED_* | MTLPixelFormat* | VK_FORMAT_*_BLOCK + * -------------------------------------------------------------------------------------- + * kETC2_RGB8_UNORM | ETC1_RGB8 | ETC2_RGB8 (iOS-only) | ETC2_R8G8B8_UNORM + * | RGB8_ETC2 | | + * -------------------------------------------------------------------------------------- + * kBC1_RGB8_UNORM | RGB_S3TC_DXT1_EXT | N/A | BC1_RGB_UNORM + * -------------------------------------------------------------------------------------- + * kBC1_RGBA8_UNORM | RGBA_S3TC_DXT1_EXT | BC1_RGBA (macOS-only)| BC1_RGBA_UNORM + */ +enum class SkTextureCompressionType { + kNone, + kETC2_RGB8_UNORM, + + kBC1_RGB8_UNORM, + kBC1_RGBA8_UNORM, + kLast = kBC1_RGBA8_UNORM, + kETC1_RGB8 = kETC2_RGB8_UNORM, +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTileMode.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTileMode.h new file mode 100644 index 00000000000..8a9d0209589 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTileMode.h @@ -0,0 +1,41 @@ +/* + * Copyright 2019 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTileModes_DEFINED +#define SkTileModes_DEFINED + +#include "include/core/SkTypes.h" + +enum class SkTileMode { + /** + * Replicate the edge color if the shader draws outside of its + * original bounds. + */ + kClamp, + + /** + * Repeat the shader's image horizontally and vertically. + */ + kRepeat, + + /** + * Repeat the shader's image horizontally and vertically, alternating + * mirror images so that adjacent images always seam. + */ + kMirror, + + /** + * Only draw within the original domain, return transparent-black everywhere else. + */ + kDecal, + + kLastTileMode = kDecal, +}; + +static constexpr int kSkTileModeCount = static_cast(SkTileMode::kLastTileMode) + 1; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTiledImageUtils.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTiledImageUtils.h new file mode 100644 index 00000000000..fc5a4f25c5a --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTiledImageUtils.h @@ -0,0 +1,125 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTiledImageUtils_DEFINED +#define SkTiledImageUtils_DEFINED + +#include "include/core/SkCanvas.h" +#include "include/core/SkImage.h" +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkSamplingOptions.h" +#include "include/core/SkScalar.h" +#include "include/private/base/SkAPI.h" + +#include + +class SkPaint; + +/** \namespace SkTiledImageUtils + SkTiledImageUtils' DrawImage/DrawImageRect methods are intended to be direct replacements + for their SkCanvas equivalents. The SkTiledImageUtils calls will break SkBitmap-backed + SkImages into smaller tiles and draw them if the original image is too large to be + uploaded to the GPU. If the original image doesn't need tiling or is already gpu-backed + the DrawImage/DrawImageRect calls will fall through to the matching SkCanvas call. +*/ +namespace SkTiledImageUtils { + +SK_API void DrawImageRect(SkCanvas* canvas, + const SkImage* image, + const SkRect& src, + const SkRect& dst, + const SkSamplingOptions& sampling = {}, + const SkPaint* paint = nullptr, + SkCanvas::SrcRectConstraint constraint = + SkCanvas::kFast_SrcRectConstraint); + +inline void DrawImageRect(SkCanvas* canvas, + const sk_sp& image, + const SkRect& src, + const SkRect& dst, + const SkSamplingOptions& sampling = {}, + const SkPaint* paint = nullptr, + SkCanvas::SrcRectConstraint constraint = + SkCanvas::kFast_SrcRectConstraint) { + DrawImageRect(canvas, image.get(), src, dst, sampling, paint, constraint); +} + +inline void DrawImageRect(SkCanvas* canvas, + const SkImage* image, + const SkRect& dst, + const SkSamplingOptions& sampling = {}, + const SkPaint* paint = nullptr, + SkCanvas::SrcRectConstraint constraint = + SkCanvas::kFast_SrcRectConstraint) { + if (!image) { + return; + } + + SkRect src = SkRect::MakeIWH(image->width(), image->height()); + + DrawImageRect(canvas, image, src, dst, sampling, paint, constraint); +} + +inline void DrawImageRect(SkCanvas* canvas, + const sk_sp& image, + const SkRect& dst, + const SkSamplingOptions& sampling = {}, + const SkPaint* paint = nullptr, + SkCanvas::SrcRectConstraint constraint = + SkCanvas::kFast_SrcRectConstraint) { + DrawImageRect(canvas, image.get(), dst, sampling, paint, constraint); +} + +inline void DrawImage(SkCanvas* canvas, + const SkImage* image, + SkScalar x, SkScalar y, + const SkSamplingOptions& sampling = {}, + const SkPaint* paint = nullptr, + SkCanvas::SrcRectConstraint constraint = + SkCanvas::kFast_SrcRectConstraint) { + if (!image) { + return; + } + + SkRect src = SkRect::MakeIWH(image->width(), image->height()); + SkRect dst = SkRect::MakeXYWH(x, y, image->width(), image->height()); + + DrawImageRect(canvas, image, src, dst, sampling, paint, constraint); +} + +inline void DrawImage(SkCanvas* canvas, + const sk_sp& image, + SkScalar x, SkScalar y, + const SkSamplingOptions& sampling = {}, + const SkPaint* paint = nullptr, + SkCanvas::SrcRectConstraint constraint = + SkCanvas::kFast_SrcRectConstraint) { + DrawImage(canvas, image.get(), x, y, sampling, paint, constraint); +} + +static constexpr int kNumImageKeyValues = 6; + +/** Retrieves a set of values that can be used as part of a cache key for the provided image. + + Unfortunately, SkImage::uniqueID isn't sufficient as an SkImage cache key. In particular, + SkBitmap-backed SkImages can share a single SkBitmap and refer to different subsets of it. + In this situation the optimal key is based on the SkBitmap's generation ID and the subset + rectangle. + For Picture-backed images this method will attempt to generate a concise internally-based + key (i.e., containing picture ID, matrix translation, width and height, etc.). For complicated + Picture-backed images (i.e., those w/ a paint or a full matrix) it will fall back to + using 'image's unique key. + + @param image The image for which key values are desired + @param keyValues The resulting key values +*/ +SK_API void GetImageKeyValues(const SkImage* image, uint32_t keyValues[kNumImageKeyValues]); + +} // namespace SkTiledImageUtils + +#endif // SkTiledImageUtils_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTraceMemoryDump.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTraceMemoryDump.h new file mode 100644 index 00000000000..d01b01bd686 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTraceMemoryDump.h @@ -0,0 +1,114 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTraceMemoryDump_DEFINED +#define SkTraceMemoryDump_DEFINED + +#include "include/core/SkTypes.h" + +class SkDiscardableMemory; + +/** + * Interface for memory tracing. + * This interface is meant to be passed as argument to the memory dump methods of Skia objects. + * The implementation of this interface is provided by the embedder. + */ +class SK_API SkTraceMemoryDump { +public: + /** + * Enum to specify the level of the requested details for the dump from the Skia objects. + */ + enum LevelOfDetail { + // Dump only the minimal details to get the total memory usage (Usually just the totals). + kLight_LevelOfDetail, + + // Dump the detailed breakdown of the objects in the caches. + kObjectsBreakdowns_LevelOfDetail + }; + + /** + * Appends a new memory dump (i.e. a row) to the trace memory infrastructure. + * If dumpName does not exist yet, a new one is created. Otherwise, a new column is appended to + * the previously created dump. + * Arguments: + * dumpName: an absolute, slash-separated, name for the item being dumped + * e.g., "skia/CacheX/EntryY". + * valueName: a string indicating the name of the column. + * e.g., "size", "active_size", "number_of_objects". + * This string is supposed to be long lived and is NOT copied. + * units: a string indicating the units for the value. + * e.g., "bytes", "objects". + * This string is supposed to be long lived and is NOT copied. + * value: the actual value being dumped. + */ + virtual void dumpNumericValue(const char* dumpName, + const char* valueName, + const char* units, + uint64_t value) = 0; + + virtual void dumpStringValue(const char* /*dumpName*/, + const char* /*valueName*/, + const char* /*value*/) { } + + /** + * Sets the memory backing for an existing dump. + * backingType and backingObjectId are used by the embedder to associate the memory dumped via + * dumpNumericValue with the corresponding dump that backs the memory. + */ + virtual void setMemoryBacking(const char* dumpName, + const char* backingType, + const char* backingObjectId) = 0; + + /** + * Specialization for memory backed by discardable memory. + */ + virtual void setDiscardableMemoryBacking( + const char* dumpName, + const SkDiscardableMemory& discardableMemoryObject) = 0; + + /** + * Returns the type of details requested in the dump. The granularity of the dump is supposed to + * match the LevelOfDetail argument. The level of detail must not affect the total size + * reported, but only granularity of the child entries. + */ + virtual LevelOfDetail getRequestedDetails() const = 0; + + /** + * Returns true if we should dump wrapped objects. Wrapped objects come from outside Skia, and + * may be independently tracked there. + */ + virtual bool shouldDumpWrappedObjects() const { return true; } + + /** + * If shouldDumpWrappedObjects() returns true then this function will be called to populate + * the output with information on whether the item being dumped is a wrapped object. + */ + virtual void dumpWrappedState(const char* /*dumpName*/, bool /*isWrappedObject*/) {} + + /** + * Returns true if we should dump unbudgeted objects. Unbudgeted objects can either come from + * wrapped objects passed into Skia from the client or from Skia created objects currently held + * by the client in a public Skia object (e.g. SkSurface or SkImage). This call is only used + * when dumping Graphite memory statistics. + */ + virtual bool shouldDumpUnbudgetedObjects() const { return true; } + + /** + * If shouldDumpUnbudgetedObjects() returns true then this function will be called to populate + * the output with information on whether the item being dumped is budgeted. This call is only + * used when dumping Graphite memory statistics. + */ + virtual void dumpBudgetedState(const char* /*dumpName*/, bool /*isBudgeted*/) {} + +protected: + virtual ~SkTraceMemoryDump() = default; + SkTraceMemoryDump() = default; + SkTraceMemoryDump(const SkTraceMemoryDump&) = delete; + SkTraceMemoryDump& operator=(const SkTraceMemoryDump&) = delete; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTypeface.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTypeface.h new file mode 100644 index 00000000000..d35396a9f2b --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTypeface.h @@ -0,0 +1,434 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTypeface_DEFINED +#define SkTypeface_DEFINED + +#include "include/core/SkFontArguments.h" +#include "include/core/SkFontParameters.h" +#include "include/core/SkFontStyle.h" +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkString.h" +#include "include/core/SkTypes.h" +#include "include/private/SkWeakRefCnt.h" +#include "include/private/base/SkOnce.h" + +#include +#include +#include + +class SkData; +class SkDescriptor; +class SkFontMgr; +class SkFontDescriptor; +class SkScalerContext; +class SkStream; +class SkStreamAsset; +class SkWStream; +enum class SkTextEncoding; +struct SkAdvancedTypefaceMetrics; +struct SkScalerContextEffects; +struct SkScalerContextRec; + +using SkTypefaceID = uint32_t; + +/** Machine endian. */ +typedef uint32_t SkFontTableTag; + +/** \class SkTypeface + + The SkTypeface class specifies the typeface and intrinsic style of a font. + This is used in the paint, along with optionally algorithmic settings like + textSize, textSkewX, textScaleX, kFakeBoldText_Mask, to specify + how text appears when drawn (and measured). + + Typeface objects are immutable, and so they can be shared between threads. +*/ +class SK_API SkTypeface : public SkWeakRefCnt { +public: + /** Returns the typeface's intrinsic style attributes. */ + SkFontStyle fontStyle() const { + return fStyle; + } + + /** Returns true if style() has the kBold bit set. */ + bool isBold() const { return fStyle.weight() >= SkFontStyle::kSemiBold_Weight; } + + /** Returns true if style() has the kItalic bit set. */ + bool isItalic() const { return fStyle.slant() != SkFontStyle::kUpright_Slant; } + + /** Returns true if the typeface claims to be fixed-pitch. + * This is a style bit, advance widths may vary even if this returns true. + */ + bool isFixedPitch() const { return fIsFixedPitch; } + + /** Copy into 'coordinates' (allocated by the caller) the design variation coordinates. + * + * @param coordinates the buffer into which to write the design variation coordinates. + * @param coordinateCount the number of entries available through 'coordinates'. + * + * @return The number of axes, or -1 if there is an error. + * If 'coordinates != nullptr' and 'coordinateCount >= numAxes' then 'coordinates' will be + * filled with the variation coordinates describing the position of this typeface in design + * variation space. It is possible the number of axes can be retrieved but actual position + * cannot. + */ + int getVariationDesignPosition(SkFontArguments::VariationPosition::Coordinate coordinates[], + int coordinateCount) const; + + /** Copy into 'parameters' (allocated by the caller) the design variation parameters. + * + * @param parameters the buffer into which to write the design variation parameters. + * @param coordinateCount the number of entries available through 'parameters'. + * + * @return The number of axes, or -1 if there is an error. + * If 'parameters != nullptr' and 'parameterCount >= numAxes' then 'parameters' will be + * filled with the variation parameters describing the position of this typeface in design + * variation space. It is possible the number of axes can be retrieved but actual parameters + * cannot. + */ + int getVariationDesignParameters(SkFontParameters::Variation::Axis parameters[], + int parameterCount) const; + + /** Return a 32bit value for this typeface, unique for the underlying font + data. Will never return 0. + */ + SkTypefaceID uniqueID() const { return fUniqueID; } + + /** Returns true if the two typefaces reference the same underlying font, + handling either being null (treating null as not equal to any font). + */ + static bool Equal(const SkTypeface* facea, const SkTypeface* faceb); + + /** Returns a non-null typeface which contains no glyphs. */ + static sk_sp MakeEmpty(); + + /** Return a new typeface based on this typeface but parameterized as specified in the + SkFontArguments. If the SkFontArguments does not supply an argument for a parameter + in the font then the value from this typeface will be used as the value for that + argument. If the cloned typeface would be exaclty the same as this typeface then + this typeface may be ref'ed and returned. May return nullptr on failure. + */ + sk_sp makeClone(const SkFontArguments&) const; + + /** + * A typeface can serialize just a descriptor (names, etc.), or it can also include the + * actual font data (which can be large). This enum controls how serialize() decides what + * to serialize. + */ + enum class SerializeBehavior { + kDoIncludeData, + kDontIncludeData, + kIncludeDataIfLocal, + }; + + /** Write a unique signature to a stream, sufficient to reconstruct a + typeface referencing the same font when Deserialize is called. + */ + void serialize(SkWStream*, SerializeBehavior = SerializeBehavior::kIncludeDataIfLocal) const; + + /** + * Same as serialize(SkWStream*, ...) but returns the serialized data in SkData, instead of + * writing it to a stream. + */ + sk_sp serialize(SerializeBehavior = SerializeBehavior::kIncludeDataIfLocal) const; + + /** Given the data previously written by serialize(), return a new instance + of a typeface referring to the same font. If that font is not available, + return nullptr. + Goes through all registered typeface factories and lastResortMgr (if non-null). + Does not affect ownership of SkStream. + */ + + static sk_sp MakeDeserialize(SkStream*, sk_sp lastResortMgr); + + /** + * Given an array of UTF32 character codes, return their corresponding glyph IDs. + * + * @param chars pointer to the array of UTF32 chars + * @param number of chars and glyphs + * @param glyphs returns the corresponding glyph IDs for each character. + */ + void unicharsToGlyphs(const SkUnichar uni[], int count, SkGlyphID glyphs[]) const; + + int textToGlyphs(const void* text, size_t byteLength, SkTextEncoding encoding, + SkGlyphID glyphs[], int maxGlyphCount) const; + + /** + * Return the glyphID that corresponds to the specified unicode code-point + * (in UTF32 encoding). If the unichar is not supported, returns 0. + * + * This is a short-cut for calling unicharsToGlyphs(). + */ + SkGlyphID unicharToGlyph(SkUnichar unichar) const; + + /** + * Return the number of glyphs in the typeface. + */ + int countGlyphs() const; + + // Table getters -- may fail if the underlying font format is not organized + // as 4-byte tables. + + /** Return the number of tables in the font. */ + int countTables() const; + + /** Copy into tags[] (allocated by the caller) the list of table tags in + * the font, and return the number. This will be the same as CountTables() + * or 0 if an error occured. If tags == NULL, this only returns the count + * (the same as calling countTables()). + */ + int getTableTags(SkFontTableTag tags[]) const; + + /** Given a table tag, return the size of its contents, or 0 if not present + */ + size_t getTableSize(SkFontTableTag) const; + + /** Copy the contents of a table into data (allocated by the caller). Note + * that the contents of the table will be in their native endian order + * (which for most truetype tables is big endian). If the table tag is + * not found, or there is an error copying the data, then 0 is returned. + * If this happens, it is possible that some or all of the memory pointed + * to by data may have been written to, even though an error has occured. + * + * @param tag The table tag whose contents are to be copied + * @param offset The offset in bytes into the table's contents where the + * copy should start from. + * @param length The number of bytes, starting at offset, of table data + * to copy. + * @param data storage address where the table contents are copied to + * @return the number of bytes actually copied into data. If offset+length + * exceeds the table's size, then only the bytes up to the table's + * size are actually copied, and this is the value returned. If + * offset > the table's size, or tag is not a valid table, + * then 0 is returned. + */ + size_t getTableData(SkFontTableTag tag, size_t offset, size_t length, + void* data) const; + + /** + * Return an immutable copy of the requested font table, or nullptr if that table was + * not found. This can sometimes be faster than calling getTableData() twice: once to find + * the length, and then again to copy the data. + * + * @param tag The table tag whose contents are to be copied + * @return an immutable copy of the table's data, or nullptr. + */ + sk_sp copyTableData(SkFontTableTag tag) const; + + /** + * Return the units-per-em value for this typeface, or zero if there is an + * error. + */ + int getUnitsPerEm() const; + + /** + * Given a run of glyphs, return the associated horizontal adjustments. + * Adjustments are in "design units", which are integers relative to the + * typeface's units per em (see getUnitsPerEm). + * + * Some typefaces are known to never support kerning. Calling this method + * with all zeros (e.g. getKerningPairAdustments(NULL, 0, NULL)) returns + * a boolean indicating if the typeface might support kerning. If it + * returns false, then it will always return false (no kerning) for all + * possible glyph runs. If it returns true, then it *may* return true for + * somne glyph runs. + * + * If count is non-zero, then the glyphs parameter must point to at least + * [count] valid glyph IDs, and the adjustments parameter must be + * sized to at least [count - 1] entries. If the method returns true, then + * [count-1] entries in the adjustments array will be set. If the method + * returns false, then no kerning should be applied, and the adjustments + * array will be in an undefined state (possibly some values may have been + * written, but none of them should be interpreted as valid values). + */ + bool getKerningPairAdjustments(const SkGlyphID glyphs[], int count, + int32_t adjustments[]) const; + + struct LocalizedString { + SkString fString; + SkString fLanguage; + }; + class LocalizedStrings { + public: + LocalizedStrings() = default; + virtual ~LocalizedStrings() { } + virtual bool next(LocalizedString* localizedString) = 0; + void unref() { delete this; } + + private: + LocalizedStrings(const LocalizedStrings&) = delete; + LocalizedStrings& operator=(const LocalizedStrings&) = delete; + }; + /** + * Returns an iterator which will attempt to enumerate all of the + * family names specified by the font. + * It is the caller's responsibility to unref() the returned pointer. + */ + LocalizedStrings* createFamilyNameIterator() const; + + /** + * Return the family name for this typeface. It will always be returned + * encoded as UTF8, but the language of the name is whatever the host + * platform chooses. + */ + void getFamilyName(SkString* name) const; + + /** + * Return the PostScript name for this typeface. + * Value may change based on variation parameters. + * Returns false if no PostScript name is available. + */ + bool getPostScriptName(SkString* name) const; + + /** + * Return a stream for the contents of the font data, or NULL on failure. + * If ttcIndex is not null, it is set to the TrueTypeCollection index + * of this typeface within the stream, or 0 if the stream is not a + * collection. + * The caller is responsible for deleting the stream. + */ + std::unique_ptr openStream(int* ttcIndex) const; + + /** + * Return a stream for the contents of the font data. + * Returns nullptr on failure or if the font data isn't already available in stream form. + * Use when the stream can be used opportunistically but the calling code would prefer + * to fall back to table access if creating the stream would be expensive. + * Otherwise acts the same as openStream. + */ + std::unique_ptr openExistingStream(int* ttcIndex) const; + + /** + * Return a scalercontext for the given descriptor. It may return a + * stub scalercontext that will not crash, but will draw nothing. + */ + std::unique_ptr createScalerContext(const SkScalerContextEffects&, + const SkDescriptor*) const; + + /** + * Return a rectangle (scaled to 1-pt) that represents the union of the bounds of all + * of the glyphs, but each one positioned at (0,). This may be conservatively large, and + * will not take into account any hinting or other size-specific adjustments. + */ + SkRect getBounds() const; + + // PRIVATE / EXPERIMENTAL -- do not call + void filterRec(SkScalerContextRec* rec) const { + this->onFilterRec(rec); + } + // PRIVATE / EXPERIMENTAL -- do not call + void getFontDescriptor(SkFontDescriptor* desc, bool* isLocal) const { + this->onGetFontDescriptor(desc, isLocal); + } + // PRIVATE / EXPERIMENTAL -- do not call + void* internal_private_getCTFontRef() const { + return this->onGetCTFontRef(); + } + + /* Skia reserves all tags that begin with a lower case letter and 0 */ + using FactoryId = SkFourByteTag; + static void Register( + FactoryId id, + sk_sp (*make)(std::unique_ptr, const SkFontArguments&)); + +protected: + explicit SkTypeface(const SkFontStyle& style, bool isFixedPitch = false); + ~SkTypeface() override; + + virtual sk_sp onMakeClone(const SkFontArguments&) const = 0; + + /** Sets the fixedPitch bit. If used, must be called in the constructor. */ + void setIsFixedPitch(bool isFixedPitch) { fIsFixedPitch = isFixedPitch; } + /** Sets the font style. If used, must be called in the constructor. */ + void setFontStyle(SkFontStyle style) { fStyle = style; } + + // Must return a valid scaler context. It can not return nullptr. + virtual std::unique_ptr onCreateScalerContext(const SkScalerContextEffects&, + const SkDescriptor*) const = 0; + virtual void onFilterRec(SkScalerContextRec*) const = 0; + friend class SkScalerContext; // onFilterRec + + // Subclasses *must* override this method to work with the PDF backend. + virtual std::unique_ptr onGetAdvancedMetrics() const = 0; + // For type1 postscript fonts only, set the glyph names for each glyph. + // destination array is non-null, and points to an array of size this->countGlyphs(). + // Backends that do not suport type1 fonts should not override. + virtual void getPostScriptGlyphNames(SkString*) const = 0; + + // The mapping from glyph to Unicode; array indices are glyph ids. + // For each glyph, give the default Unicode value, if it exists. + // dstArray is non-null, and points to an array of size this->countGlyphs(). + virtual void getGlyphToUnicodeMap(SkUnichar* dstArray) const = 0; + + virtual std::unique_ptr onOpenStream(int* ttcIndex) const = 0; + + virtual std::unique_ptr onOpenExistingStream(int* ttcIndex) const; + + virtual bool onGlyphMaskNeedsCurrentColor() const = 0; + + virtual int onGetVariationDesignPosition( + SkFontArguments::VariationPosition::Coordinate coordinates[], + int coordinateCount) const = 0; + + virtual int onGetVariationDesignParameters( + SkFontParameters::Variation::Axis parameters[], int parameterCount) const = 0; + + virtual void onGetFontDescriptor(SkFontDescriptor*, bool* isLocal) const = 0; + + virtual void onCharsToGlyphs(const SkUnichar* chars, int count, SkGlyphID glyphs[]) const = 0; + virtual int onCountGlyphs() const = 0; + + virtual int onGetUPEM() const = 0; + virtual bool onGetKerningPairAdjustments(const SkGlyphID glyphs[], int count, + int32_t adjustments[]) const; + + /** Returns the family name of the typeface as known by its font manager. + * This name may or may not be produced by the family name iterator. + */ + virtual void onGetFamilyName(SkString* familyName) const = 0; + virtual bool onGetPostScriptName(SkString*) const = 0; + + /** Returns an iterator over the family names in the font. */ + virtual LocalizedStrings* onCreateFamilyNameIterator() const = 0; + + virtual int onGetTableTags(SkFontTableTag tags[]) const = 0; + virtual size_t onGetTableData(SkFontTableTag, size_t offset, + size_t length, void* data) const = 0; + virtual sk_sp onCopyTableData(SkFontTableTag) const; + + virtual bool onComputeBounds(SkRect*) const; + + virtual void* onGetCTFontRef() const { return nullptr; } + +private: + /** Returns true if the typeface's glyph masks may refer to the foreground + * paint foreground color. This is needed to determine caching requirements. Usually true for + * typefaces that contain a COLR table. + */ + bool glyphMaskNeedsCurrentColor() const; + friend class SkStrikeServerImpl; // glyphMaskNeedsCurrentColor + friend class SkTypefaceProxyPrototype; // glyphMaskNeedsCurrentColor + + /** Retrieve detailed typeface metrics. Used by the PDF backend. */ + std::unique_ptr getAdvancedMetrics() const; + friend class SkRandomTypeface; // getAdvancedMetrics + friend class SkPDFFont; // getAdvancedMetrics + + friend class SkFontPriv; // getGlyphToUnicodeMap + +private: + SkTypefaceID fUniqueID; + SkFontStyle fStyle; + mutable SkRect fBounds; + mutable SkOnce fBoundsOnce; + bool fIsFixedPitch; + + using INHERITED = SkWeakRefCnt; +}; +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTypes.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTypes.h new file mode 100644 index 00000000000..722e7354ffb --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkTypes.h @@ -0,0 +1,199 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTypes_DEFINED +#define SkTypes_DEFINED + +// All of these files should be independent of things users can set via the user config file. +// They should also be able to be included in any order. +// IWYU pragma: begin_exports +#include "include/private/base/SkFeatures.h" + +// Load and verify defines from the user config file. +#include "include/private/base/SkLoadUserConfig.h" + +// Any includes or defines below can be configured by the user config file. +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkAssert.h" +#include "include/private/base/SkAttributes.h" +#include "include/private/base/SkDebug.h" +// IWYU pragma: end_exports + +#include +#include + +#if !defined(SK_GANESH) && !defined(SK_GRAPHITE) +# undef SK_GL +# undef SK_VULKAN +# undef SK_METAL +# undef SK_DAWN +# undef SK_DIRECT3D +#endif + +// If SK_R32_SHIFT is set, we'll use that to choose RGBA or BGRA. +// If not, we'll default to RGBA everywhere except BGRA on Windows. +#if defined(SK_R32_SHIFT) + static_assert(SK_R32_SHIFT == 0 || SK_R32_SHIFT == 16, ""); +#elif defined(SK_BUILD_FOR_WIN) + #define SK_R32_SHIFT 16 +#else + #define SK_R32_SHIFT 0 +#endif + +#if defined(SK_B32_SHIFT) + static_assert(SK_B32_SHIFT == (16-SK_R32_SHIFT), ""); +#else + #define SK_B32_SHIFT (16-SK_R32_SHIFT) +#endif + +#define SK_G32_SHIFT 8 +#define SK_A32_SHIFT 24 + +/** + * SK_PMCOLOR_BYTE_ORDER can be used to query the byte order of SkPMColor at compile time. + */ +#ifdef SK_CPU_BENDIAN +# define SK_PMCOLOR_BYTE_ORDER(C0, C1, C2, C3) \ + (SK_ ## C3 ## 32_SHIFT == 0 && \ + SK_ ## C2 ## 32_SHIFT == 8 && \ + SK_ ## C1 ## 32_SHIFT == 16 && \ + SK_ ## C0 ## 32_SHIFT == 24) +#else +# define SK_PMCOLOR_BYTE_ORDER(C0, C1, C2, C3) \ + (SK_ ## C0 ## 32_SHIFT == 0 && \ + SK_ ## C1 ## 32_SHIFT == 8 && \ + SK_ ## C2 ## 32_SHIFT == 16 && \ + SK_ ## C3 ## 32_SHIFT == 24) +#endif + +#if defined SK_DEBUG && defined SK_BUILD_FOR_WIN + #ifdef free + #undef free + #endif + #include + #undef free +#endif + +#ifndef SK_ALLOW_STATIC_GLOBAL_INITIALIZERS + #define SK_ALLOW_STATIC_GLOBAL_INITIALIZERS 0 +#endif + +#if !defined(SK_GAMMA_EXPONENT) + #define SK_GAMMA_EXPONENT (0.0f) // SRGB +#endif + +#if !defined(SK_GAMMA_CONTRAST) + // A value of 0.5 for SK_GAMMA_CONTRAST appears to be a good compromise. + // With lower values small text appears washed out (though correctly so). + // With higher values lcd fringing is worse and the smoothing effect of + // partial coverage is diminished. + #define SK_GAMMA_CONTRAST (0.5f) +#endif + +#if defined(SK_HISTOGRAM_ENUMERATION) || \ + defined(SK_HISTOGRAM_BOOLEAN) || \ + defined(SK_HISTOGRAM_EXACT_LINEAR) || \ + defined(SK_HISTOGRAM_MEMORY_KB) +# define SK_HISTOGRAMS_ENABLED 1 +#else +# define SK_HISTOGRAMS_ENABLED 0 +#endif + +#ifndef SK_HISTOGRAM_BOOLEAN +# define SK_HISTOGRAM_BOOLEAN(name, sample) +#endif + +#ifndef SK_HISTOGRAM_ENUMERATION +# define SK_HISTOGRAM_ENUMERATION(name, sample, enum_size) +#endif + +#ifndef SK_HISTOGRAM_EXACT_LINEAR +# define SK_HISTOGRAM_EXACT_LINEAR(name, sample, value_max) +#endif + +#ifndef SK_HISTOGRAM_MEMORY_KB +# define SK_HISTOGRAM_MEMORY_KB(name, sample) +#endif + +#define SK_HISTOGRAM_PERCENTAGE(name, percent_as_int) \ + SK_HISTOGRAM_EXACT_LINEAR(name, percent_as_int, 101) + +// The top-level define SK_ENABLE_OPTIMIZE_SIZE can be used to remove several large features at once +#if defined(SK_ENABLE_OPTIMIZE_SIZE) + #if !defined(SK_FORCE_RASTER_PIPELINE_BLITTER) + #define SK_FORCE_RASTER_PIPELINE_BLITTER + #endif + #define SK_DISABLE_SDF_TEXT +#endif + +#ifndef SK_DISABLE_LEGACY_SHADERCONTEXT +# define SK_ENABLE_LEGACY_SHADERCONTEXT +#endif + +#if defined(SK_BUILD_FOR_LIBFUZZER) || defined(SK_BUILD_FOR_AFL_FUZZ) +#if !defined(SK_BUILD_FOR_FUZZER) + #define SK_BUILD_FOR_FUZZER +#endif +#endif + +/** + * These defines are set to 0 or 1, rather than being undefined or defined + * TODO: consider updating these for consistency + */ + +#if !defined(GR_CACHE_STATS) + #if defined(SK_DEBUG) || defined(SK_DUMP_STATS) + #define GR_CACHE_STATS 1 + #else + #define GR_CACHE_STATS 0 + #endif +#endif + +#if !defined(GR_GPU_STATS) + #if defined(SK_DEBUG) || defined(SK_DUMP_STATS) || defined(GR_TEST_UTILS) + #define GR_GPU_STATS 1 + #else + #define GR_GPU_STATS 0 + #endif +#endif + +//////////////////////////////////////////////////////////////////////////////// + +typedef uint32_t SkFourByteTag; +static inline constexpr SkFourByteTag SkSetFourByteTag(char a, char b, char c, char d) { + return (((uint32_t)a << 24) | ((uint32_t)b << 16) | ((uint32_t)c << 8) | (uint32_t)d); +} + +//////////////////////////////////////////////////////////////////////////////// + +/** 32 bit integer to hold a unicode value +*/ +typedef int32_t SkUnichar; + +/** 16 bit unsigned integer to hold a glyph index +*/ +typedef uint16_t SkGlyphID; + +/** 32 bit value to hold a millisecond duration + Note that SK_MSecMax is about 25 days. +*/ +typedef uint32_t SkMSec; + +/** Maximum representable milliseconds; 24d 20h 31m 23.647s. +*/ +static constexpr SkMSec SK_MSecMax = INT32_MAX; + +/** The generation IDs in Skia reserve 0 has an invalid marker. +*/ +static constexpr uint32_t SK_InvalidGenID = 0; + +/** The unique IDs in Skia reserve 0 has an invalid marker. +*/ +static constexpr uint32_t SK_InvalidUniqueID = 0; + + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkUnPreMultiply.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkUnPreMultiply.h new file mode 100644 index 00000000000..649c89f9cc6 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkUnPreMultiply.h @@ -0,0 +1,55 @@ + +/* + * Copyright 2008 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkUnPreMultiply_DEFINED +#define SkUnPreMultiply_DEFINED + +#include "include/core/SkColor.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkCPUTypes.h" + +#include + +class SK_API SkUnPreMultiply { +public: + typedef uint32_t Scale; + + // index this table with alpha [0..255] + static const Scale* GetScaleTable() { + return gTable; + } + + static Scale GetScale(U8CPU alpha) { + SkASSERT(alpha <= 255); + return gTable[alpha]; + } + + /** Usage: + + const Scale* table = SkUnPreMultiply::GetScaleTable(); + + for (...) { + unsigned a = ... + SkUnPreMultiply::Scale scale = table[a]; + + red = SkUnPreMultiply::ApplyScale(scale, red); + ... + // now red is unpremultiplied + } + */ + static U8CPU ApplyScale(Scale scale, U8CPU component) { + SkASSERT(component <= 255); + return (scale * component + (1 << 23)) >> 24; + } + + static SkColor PMColorToColor(SkPMColor c); + +private: + static const uint32_t gTable[256]; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkVertices.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkVertices.h new file mode 100644 index 00000000000..0f17e452166 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkVertices.h @@ -0,0 +1,136 @@ +/* + * Copyright 2017 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkVertices_DEFINED +#define SkVertices_DEFINED + +#include "include/core/SkColor.h" +#include "include/core/SkPoint.h" +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +#include +#include +#include + +class SkVerticesPriv; + +/** + * An immutable set of vertex data that can be used with SkCanvas::drawVertices. + */ +class SK_API SkVertices : public SkNVRefCnt { + struct Desc; + struct Sizes; +public: + enum VertexMode { + kTriangles_VertexMode, + kTriangleStrip_VertexMode, + kTriangleFan_VertexMode, + + kLast_VertexMode = kTriangleFan_VertexMode, + }; + + /** + * Create a vertices by copying the specified arrays. texs, colors may be nullptr, + * and indices is ignored if indexCount == 0. + */ + static sk_sp MakeCopy(VertexMode mode, int vertexCount, + const SkPoint positions[], + const SkPoint texs[], + const SkColor colors[], + int indexCount, + const uint16_t indices[]); + + static sk_sp MakeCopy(VertexMode mode, int vertexCount, + const SkPoint positions[], + const SkPoint texs[], + const SkColor colors[]) { + return MakeCopy(mode, + vertexCount, + positions, + texs, + colors, + 0, + nullptr); + } + + enum BuilderFlags { + kHasTexCoords_BuilderFlag = 1 << 0, + kHasColors_BuilderFlag = 1 << 1, + }; + class SK_API Builder { + public: + Builder(VertexMode mode, int vertexCount, int indexCount, uint32_t flags); + + bool isValid() const { return fVertices != nullptr; } + + SkPoint* positions(); + uint16_t* indices(); // returns null if there are no indices + + // If we have custom attributes, these will always be null + SkPoint* texCoords(); // returns null if there are no texCoords + SkColor* colors(); // returns null if there are no colors + + // Detach the built vertices object. After the first call, this will always return null. + sk_sp detach(); + + private: + Builder(const Desc&); + + void init(const Desc&); + + // holds a partially complete object. only completed in detach() + sk_sp fVertices; + // Extra storage for intermediate vertices in the case where the client specifies indexed + // triangle fans. These get converted to indexed triangles when the Builder is finalized. + std::unique_ptr fIntermediateFanIndices; + + friend class SkVertices; + friend class SkVerticesPriv; + }; + + uint32_t uniqueID() const { return fUniqueID; } + const SkRect& bounds() const { return fBounds; } + + // returns approximate byte size of the vertices object + size_t approximateSize() const; + + // Provides access to functions that aren't part of the public API. + SkVerticesPriv priv(); + const SkVerticesPriv priv() const; // NOLINT(readability-const-return-type) + +private: + SkVertices() {} + + friend class SkVerticesPriv; + + // these are needed since we've manually sized our allocation (see Builder::init) + friend class SkNVRefCnt; + void operator delete(void* p); + + Sizes getSizes() const; + + // we store this first, to pair with the refcnt in our base-class, so we don't have an + // unnecessary pad between it and the (possibly 8-byte aligned) ptrs. + uint32_t fUniqueID; + + // these point inside our allocation, so none of these can be "freed" + SkPoint* fPositions; // [vertexCount] + uint16_t* fIndices; // [indexCount] or null + SkPoint* fTexs; // [vertexCount] or null + SkColor* fColors; // [vertexCount] or null + + SkRect fBounds; // computed to be the union of the fPositions[] + int fVertexCount; + int fIndexCount; + + VertexMode fMode; + // below here is where the actual array data is stored. +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkYUVAInfo.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkYUVAInfo.h new file mode 100644 index 00000000000..bbbae5d383d --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkYUVAInfo.h @@ -0,0 +1,308 @@ +/* + * Copyright 2020 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkYUVAInfo_DEFINED +#define SkYUVAInfo_DEFINED + +#include "include/codec/SkEncodedOrigin.h" +#include "include/core/SkImageInfo.h" +#include "include/core/SkMatrix.h" +#include "include/core/SkSize.h" +#include "include/core/SkTypes.h" + +#include +#include +#include +#include + +/** + * Specifies the structure of planes for a YUV image with optional alpha. The actual planar data + * is not part of this structure and depending on usage is in external textures or pixmaps. + */ +class SK_API SkYUVAInfo { +public: + enum YUVAChannels { kY, kU, kV, kA, kLast = kA }; + static constexpr int kYUVAChannelCount = static_cast(YUVAChannels::kLast + 1); + + struct YUVALocation; // For internal use. + using YUVALocations = std::array; + + /** + * Specifies how YUV (and optionally A) are divided among planes. Planes are separated by + * underscores in the enum value names. Within each plane the pixmap/texture channels are + * mapped to the YUVA channels in the order specified, e.g. for kY_UV Y is in channel 0 of plane + * 0, U is in channel 0 of plane 1, and V is in channel 1 of plane 1. Channel ordering + * within a pixmap/texture given the channels it contains: + * A: 0:A + * Luminance/Gray: 0:Gray + * Luminance/Gray + Alpha: 0:Gray, 1:A + * RG 0:R, 1:G + * RGB 0:R, 1:G, 2:B + * RGBA 0:R, 1:G, 2:B, 3:A + */ + enum class PlaneConfig { + kUnknown, + + kY_U_V, ///< Plane 0: Y, Plane 1: U, Plane 2: V + kY_V_U, ///< Plane 0: Y, Plane 1: V, Plane 2: U + kY_UV, ///< Plane 0: Y, Plane 1: UV + kY_VU, ///< Plane 0: Y, Plane 1: VU + kYUV, ///< Plane 0: YUV + kUYV, ///< Plane 0: UYV + + kY_U_V_A, ///< Plane 0: Y, Plane 1: U, Plane 2: V, Plane 3: A + kY_V_U_A, ///< Plane 0: Y, Plane 1: V, Plane 2: U, Plane 3: A + kY_UV_A, ///< Plane 0: Y, Plane 1: UV, Plane 2: A + kY_VU_A, ///< Plane 0: Y, Plane 1: VU, Plane 2: A + kYUVA, ///< Plane 0: YUVA + kUYVA, ///< Plane 0: UYVA + + kLast = kUYVA + }; + + /** + * UV subsampling is also specified in the enum value names using J:a:b notation (e.g. 4:2:0 is + * 1/2 horizontal and 1/2 vertical resolution for U and V). If alpha is present it is not sub- + * sampled. Note that Subsampling values other than k444 are only valid with PlaneConfig values + * that have U and V in different planes than Y (and A, if present). + */ + enum class Subsampling { + kUnknown, + + k444, ///< No subsampling. UV values for each Y. + k422, ///< 1 set of UV values for each 2x1 block of Y values. + k420, ///< 1 set of UV values for each 2x2 block of Y values. + k440, ///< 1 set of UV values for each 1x2 block of Y values. + k411, ///< 1 set of UV values for each 4x1 block of Y values. + k410, ///< 1 set of UV values for each 4x2 block of Y values. + + kLast = k410 + }; + + /** + * Describes how subsampled chroma values are sited relative to luma values. + * + * Currently only centered siting is supported but will expand to support additional sitings. + */ + enum class Siting { + /** + * Subsampled chroma value is sited at the center of the block of corresponding luma values. + */ + kCentered, + }; + + static constexpr int kMaxPlanes = 4; + + /** ratio of Y/A values to U/V values in x and y. */ + static std::tuple SubsamplingFactors(Subsampling); + + /** + * SubsamplingFactors(Subsampling) if planedIdx refers to a U/V plane and otherwise {1, 1} if + * inputs are valid. Invalid inputs consist of incompatible PlaneConfig/Subsampling/planeIdx + * combinations. {0, 0} is returned for invalid inputs. + */ + static std::tuple PlaneSubsamplingFactors(PlaneConfig, Subsampling, int planeIdx); + + /** + * Given image dimensions, a planer configuration, subsampling, and origin, determine the + * expected size of each plane. Returns the number of expected planes. planeDimensions[0] + * through planeDimensions[] are written. The input image dimensions are as displayed + * (after the planes have been transformed to the intended display orientation). The plane + * dimensions are output as the planes are stored in memory (may be rotated from image + * dimensions). + */ + static int PlaneDimensions(SkISize imageDimensions, + PlaneConfig, + Subsampling, + SkEncodedOrigin, + SkISize planeDimensions[kMaxPlanes]); + + /** Number of planes for a given PlaneConfig. */ + static constexpr int NumPlanes(PlaneConfig); + + /** + * Number of Y, U, V, A channels in the ith plane for a given PlaneConfig (or 0 if i is + * invalid). + */ + static constexpr int NumChannelsInPlane(PlaneConfig, int i); + + /** + * Given a PlaneConfig and a set of channel flags for each plane, convert to YUVALocations + * representation. Fails if channel flags aren't valid for the PlaneConfig (i.e. don't have + * enough channels in a plane) by returning an invalid set of locations (plane indices are -1). + */ + static YUVALocations GetYUVALocations(PlaneConfig, const uint32_t* planeChannelFlags); + + /** Does the PlaneConfig have alpha values? */ + static bool HasAlpha(PlaneConfig); + + SkYUVAInfo() = default; + SkYUVAInfo(const SkYUVAInfo&) = default; + + /** + * 'dimensions' should specify the size of the full resolution image (after planes have been + * oriented to how the image is displayed as indicated by 'origin'). + */ + SkYUVAInfo(SkISize dimensions, + PlaneConfig, + Subsampling, + SkYUVColorSpace, + SkEncodedOrigin origin = kTopLeft_SkEncodedOrigin, + Siting sitingX = Siting::kCentered, + Siting sitingY = Siting::kCentered); + + SkYUVAInfo& operator=(const SkYUVAInfo& that) = default; + + PlaneConfig planeConfig() const { return fPlaneConfig; } + Subsampling subsampling() const { return fSubsampling; } + + std::tuple planeSubsamplingFactors(int planeIdx) const { + return PlaneSubsamplingFactors(fPlaneConfig, fSubsampling, planeIdx); + } + + /** + * Dimensions of the full resolution image (after planes have been oriented to how the image + * is displayed as indicated by fOrigin). + */ + SkISize dimensions() const { return fDimensions; } + int width() const { return fDimensions.width(); } + int height() const { return fDimensions.height(); } + + SkYUVColorSpace yuvColorSpace() const { return fYUVColorSpace; } + Siting sitingX() const { return fSitingX; } + Siting sitingY() const { return fSitingY; } + + SkEncodedOrigin origin() const { return fOrigin; } + + SkMatrix originMatrix() const { + return SkEncodedOriginToMatrix(fOrigin, this->width(), this->height()); + } + + bool hasAlpha() const { return HasAlpha(fPlaneConfig); } + + /** + * Returns the number of planes and initializes planeDimensions[0]..planeDimensions[] to + * the expected dimensions for each plane. Dimensions are as stored in memory, before + * transformation to image display space as indicated by origin(). + */ + int planeDimensions(SkISize planeDimensions[kMaxPlanes]) const { + return PlaneDimensions(fDimensions, fPlaneConfig, fSubsampling, fOrigin, planeDimensions); + } + + /** + * Given a per-plane row bytes, determine size to allocate for all planes. Optionally retrieves + * the per-plane byte sizes in planeSizes if not null. If total size overflows will return + * SIZE_MAX and set all planeSizes to SIZE_MAX. + */ + size_t computeTotalBytes(const size_t rowBytes[kMaxPlanes], + size_t planeSizes[kMaxPlanes] = nullptr) const; + + int numPlanes() const { return NumPlanes(fPlaneConfig); } + + int numChannelsInPlane(int i) const { return NumChannelsInPlane(fPlaneConfig, i); } + + /** + * Given a set of channel flags for each plane, converts this->planeConfig() to YUVALocations + * representation. Fails if the channel flags aren't valid for the PlaneConfig (i.e. don't have + * enough channels in a plane) by returning default initialized locations (all plane indices are + * -1). + */ + YUVALocations toYUVALocations(const uint32_t* channelFlags) const; + + /** + * Makes a SkYUVAInfo that is identical to this one but with the passed Subsampling. If the + * passed Subsampling is not k444 and this info's PlaneConfig is not compatible with chroma + * subsampling (because Y is in the same plane as UV) then the result will be an invalid + * SkYUVAInfo. + */ + SkYUVAInfo makeSubsampling(SkYUVAInfo::Subsampling) const; + + /** + * Makes a SkYUVAInfo that is identical to this one but with the passed dimensions. If the + * passed dimensions is empty then the result will be an invalid SkYUVAInfo. + */ + SkYUVAInfo makeDimensions(SkISize) const; + + bool operator==(const SkYUVAInfo& that) const; + bool operator!=(const SkYUVAInfo& that) const { return !(*this == that); } + + bool isValid() const { return fPlaneConfig != PlaneConfig::kUnknown; } + +private: + SkISize fDimensions = {0, 0}; + + PlaneConfig fPlaneConfig = PlaneConfig::kUnknown; + Subsampling fSubsampling = Subsampling::kUnknown; + + SkYUVColorSpace fYUVColorSpace = SkYUVColorSpace::kIdentity_SkYUVColorSpace; + + /** + * YUVA data often comes from formats like JPEG that support EXIF orientation. + * Code that operates on the raw YUV data often needs to know that orientation. + */ + SkEncodedOrigin fOrigin = kTopLeft_SkEncodedOrigin; + + Siting fSitingX = Siting::kCentered; + Siting fSitingY = Siting::kCentered; +}; + +constexpr int SkYUVAInfo::NumPlanes(PlaneConfig planeConfig) { + switch (planeConfig) { + case PlaneConfig::kUnknown: return 0; + case PlaneConfig::kY_U_V: return 3; + case PlaneConfig::kY_V_U: return 3; + case PlaneConfig::kY_UV: return 2; + case PlaneConfig::kY_VU: return 2; + case PlaneConfig::kYUV: return 1; + case PlaneConfig::kUYV: return 1; + case PlaneConfig::kY_U_V_A: return 4; + case PlaneConfig::kY_V_U_A: return 4; + case PlaneConfig::kY_UV_A: return 3; + case PlaneConfig::kY_VU_A: return 3; + case PlaneConfig::kYUVA: return 1; + case PlaneConfig::kUYVA: return 1; + } + SkUNREACHABLE; +} + +constexpr int SkYUVAInfo::NumChannelsInPlane(PlaneConfig config, int i) { + switch (config) { + case PlaneConfig::kUnknown: + return 0; + + case SkYUVAInfo::PlaneConfig::kY_U_V: + case SkYUVAInfo::PlaneConfig::kY_V_U: + return i >= 0 && i < 3 ? 1 : 0; + case SkYUVAInfo::PlaneConfig::kY_UV: + case SkYUVAInfo::PlaneConfig::kY_VU: + switch (i) { + case 0: return 1; + case 1: return 2; + default: return 0; + } + case SkYUVAInfo::PlaneConfig::kYUV: + case SkYUVAInfo::PlaneConfig::kUYV: + return i == 0 ? 3 : 0; + case SkYUVAInfo::PlaneConfig::kY_U_V_A: + case SkYUVAInfo::PlaneConfig::kY_V_U_A: + return i >= 0 && i < 4 ? 1 : 0; + case SkYUVAInfo::PlaneConfig::kY_UV_A: + case SkYUVAInfo::PlaneConfig::kY_VU_A: + switch (i) { + case 0: return 1; + case 1: return 2; + case 2: return 1; + default: return 0; + } + case SkYUVAInfo::PlaneConfig::kYUVA: + case SkYUVAInfo::PlaneConfig::kUYVA: + return i == 0 ? 4 : 0; + } + return 0; +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkYUVAPixmaps.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkYUVAPixmaps.h new file mode 100644 index 00000000000..11ec6c01a6a --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/core/SkYUVAPixmaps.h @@ -0,0 +1,337 @@ +/* + * Copyright 2020 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkYUVAPixmaps_DEFINED +#define SkYUVAPixmaps_DEFINED + +#include "include/core/SkColorType.h" +#include "include/core/SkData.h" +#include "include/core/SkImageInfo.h" +#include "include/core/SkPixmap.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkSize.h" +#include "include/core/SkTypes.h" +#include "include/core/SkYUVAInfo.h" +#include "include/private/base/SkTo.h" + +#include +#include +#include +#include + +/** + * SkYUVAInfo combined with per-plane SkColorTypes and row bytes. Fully specifies the SkPixmaps + * for a YUVA image without the actual pixel memory and data. + */ +class SK_API SkYUVAPixmapInfo { +public: + static constexpr auto kMaxPlanes = SkYUVAInfo::kMaxPlanes; + + using PlaneConfig = SkYUVAInfo::PlaneConfig; + using Subsampling = SkYUVAInfo::Subsampling; + + /** + * Data type for Y, U, V, and possibly A channels independent of how values are packed into + * planes. + **/ + enum class DataType { + kUnorm8, ///< 8 bit unsigned normalized + kUnorm16, ///< 16 bit unsigned normalized + kFloat16, ///< 16 bit (half) floating point + kUnorm10_Unorm2, ///< 10 bit unorm for Y, U, and V. 2 bit unorm for alpha (if present). + + kLast = kUnorm10_Unorm2 + }; + static constexpr int kDataTypeCnt = static_cast(DataType::kLast) + 1; + + class SK_API SupportedDataTypes { + public: + /** Defaults to nothing supported. */ + constexpr SupportedDataTypes() = default; + + /** All legal combinations of PlaneConfig and DataType are supported. */ + static constexpr SupportedDataTypes All(); + + /** + * Checks whether there is a supported combination of color types for planes structured + * as indicated by PlaneConfig with channel data types as indicated by DataType. + */ + constexpr bool supported(PlaneConfig, DataType) const; + + /** + * Update to add support for pixmaps with numChannel channels where each channel is + * represented as DataType. + */ + void enableDataType(DataType, int numChannels); + + private: + // The bit for DataType dt with n channels is at index kDataTypeCnt*(n-1) + dt. + std::bitset fDataTypeSupport = {}; + }; + + /** + * Gets the default SkColorType to use with numChannels channels, each represented as DataType. + * Returns kUnknown_SkColorType if no such color type. + */ + static constexpr SkColorType DefaultColorTypeForDataType(DataType dataType, int numChannels); + + /** + * If the SkColorType is supported for YUVA pixmaps this will return the number of YUVA channels + * that can be stored in a plane of this color type and what the DataType is of those channels. + * If the SkColorType is not supported as a YUVA plane the number of channels is reported as 0 + * and the DataType returned should be ignored. + */ + static std::tuple NumChannelsAndDataType(SkColorType); + + /** Default SkYUVAPixmapInfo is invalid. */ + SkYUVAPixmapInfo() = default; + + /** + * Initializes the SkYUVAPixmapInfo from a SkYUVAInfo with per-plane color types and row bytes. + * This will be invalid if the colorTypes aren't compatible with the SkYUVAInfo or if a + * rowBytes entry is not valid for the plane dimensions and color type. Color type and + * row byte values beyond the number of planes in SkYUVAInfo are ignored. All SkColorTypes + * must have the same DataType or this will be invalid. + * + * If rowBytes is nullptr then bpp*width is assumed for each plane. + */ + SkYUVAPixmapInfo(const SkYUVAInfo&, + const SkColorType[kMaxPlanes], + const size_t rowBytes[kMaxPlanes]); + /** + * Like above but uses DefaultColorTypeForDataType to determine each plane's SkColorType. If + * rowBytes is nullptr then bpp*width is assumed for each plane. + */ + SkYUVAPixmapInfo(const SkYUVAInfo&, DataType, const size_t rowBytes[kMaxPlanes]); + + SkYUVAPixmapInfo(const SkYUVAPixmapInfo&) = default; + + SkYUVAPixmapInfo& operator=(const SkYUVAPixmapInfo&) = default; + + bool operator==(const SkYUVAPixmapInfo&) const; + bool operator!=(const SkYUVAPixmapInfo& that) const { return !(*this == that); } + + const SkYUVAInfo& yuvaInfo() const { return fYUVAInfo; } + + SkYUVColorSpace yuvColorSpace() const { return fYUVAInfo.yuvColorSpace(); } + + /** The number of SkPixmap planes, 0 if this SkYUVAPixmapInfo is invalid. */ + int numPlanes() const { return fYUVAInfo.numPlanes(); } + + /** The per-YUV[A] channel data type. */ + DataType dataType() const { return fDataType; } + + /** + * Row bytes for the ith plane. Returns zero if i >= numPlanes() or this SkYUVAPixmapInfo is + * invalid. + */ + size_t rowBytes(int i) const { return fRowBytes[static_cast(i)]; } + + /** Image info for the ith plane, or default SkImageInfo if i >= numPlanes() */ + const SkImageInfo& planeInfo(int i) const { return fPlaneInfos[static_cast(i)]; } + + /** + * Determine size to allocate for all planes. Optionally retrieves the per-plane sizes in + * planeSizes if not null. If total size overflows will return SIZE_MAX and set all planeSizes + * to SIZE_MAX. Returns 0 and fills planesSizes with 0 if this SkYUVAPixmapInfo is not valid. + */ + size_t computeTotalBytes(size_t planeSizes[kMaxPlanes] = nullptr) const; + + /** + * Takes an allocation that is assumed to be at least computeTotalBytes() in size and configures + * the first numPlanes() entries in pixmaps array to point into that memory. The remaining + * entries of pixmaps are default initialized. Fails if this SkYUVAPixmapInfo not valid. + */ + bool initPixmapsFromSingleAllocation(void* memory, SkPixmap pixmaps[kMaxPlanes]) const; + + /** + * Returns true if this has been configured with a non-empty dimensioned SkYUVAInfo with + * compatible color types and row bytes. + */ + bool isValid() const { return fYUVAInfo.isValid(); } + + /** Is this valid and does it use color types allowed by the passed SupportedDataTypes? */ + bool isSupported(const SupportedDataTypes&) const; + +private: + SkYUVAInfo fYUVAInfo; + std::array fPlaneInfos = {}; + std::array fRowBytes = {}; + DataType fDataType = DataType::kUnorm8; + static_assert(kUnknown_SkColorType == 0, "default init isn't kUnknown"); +}; + +/** + * Helper to store SkPixmap planes as described by a SkYUVAPixmapInfo. Can be responsible for + * allocating/freeing memory for pixmaps or use external memory. + */ +class SK_API SkYUVAPixmaps { +public: + using DataType = SkYUVAPixmapInfo::DataType; + static constexpr auto kMaxPlanes = SkYUVAPixmapInfo::kMaxPlanes; + + static SkColorType RecommendedRGBAColorType(DataType); + + /** Allocate space for pixmaps' pixels in the SkYUVAPixmaps. */ + static SkYUVAPixmaps Allocate(const SkYUVAPixmapInfo& yuvaPixmapInfo); + + /** + * Use storage in SkData as backing store for pixmaps' pixels. SkData is retained by the + * SkYUVAPixmaps. + */ + static SkYUVAPixmaps FromData(const SkYUVAPixmapInfo&, sk_sp); + + /** + * Makes a deep copy of the src SkYUVAPixmaps. The returned SkYUVAPixmaps owns its planes' + * backing stores. + */ + static SkYUVAPixmaps MakeCopy(const SkYUVAPixmaps& src); + + /** + * Use passed in memory as backing store for pixmaps' pixels. Caller must ensure memory remains + * allocated while pixmaps are in use. There must be at least + * SkYUVAPixmapInfo::computeTotalBytes() allocated starting at memory. + */ + static SkYUVAPixmaps FromExternalMemory(const SkYUVAPixmapInfo&, void* memory); + + /** + * Wraps existing SkPixmaps. The SkYUVAPixmaps will have no ownership of the SkPixmaps' pixel + * memory so the caller must ensure it remains valid. Will return an invalid SkYUVAPixmaps if + * the SkYUVAInfo isn't compatible with the SkPixmap array (number of planes, plane dimensions, + * sufficient color channels in planes, ...). + */ + static SkYUVAPixmaps FromExternalPixmaps(const SkYUVAInfo&, const SkPixmap[kMaxPlanes]); + + /** Default SkYUVAPixmaps is invalid. */ + SkYUVAPixmaps() = default; + ~SkYUVAPixmaps() = default; + + SkYUVAPixmaps(SkYUVAPixmaps&& that) = default; + SkYUVAPixmaps& operator=(SkYUVAPixmaps&& that) = default; + SkYUVAPixmaps(const SkYUVAPixmaps&) = default; + SkYUVAPixmaps& operator=(const SkYUVAPixmaps& that) = default; + + /** Does have initialized pixmaps compatible with its SkYUVAInfo. */ + bool isValid() const { return !fYUVAInfo.dimensions().isEmpty(); } + + const SkYUVAInfo& yuvaInfo() const { return fYUVAInfo; } + + DataType dataType() const { return fDataType; } + + SkYUVAPixmapInfo pixmapsInfo() const; + + /** Number of pixmap planes or 0 if this SkYUVAPixmaps is invalid. */ + int numPlanes() const { return this->isValid() ? fYUVAInfo.numPlanes() : 0; } + + /** + * Access the SkPixmap planes. They are default initialized if this is not a valid + * SkYUVAPixmaps. + */ + const std::array& planes() const { return fPlanes; } + + /** + * Get the ith SkPixmap plane. SkPixmap will be default initialized if i >= numPlanes or this + * SkYUVAPixmaps is invalid. + */ + const SkPixmap& plane(int i) const { return fPlanes[SkToSizeT(i)]; } + + /** + * Computes a YUVALocations representation of the planar layout. The result is guaranteed to be + * valid if this->isValid(). + */ + SkYUVAInfo::YUVALocations toYUVALocations() const; + + /** Does this SkPixmaps own the backing store of the planes? */ + bool ownsStorage() const { return SkToBool(fData); } + +private: + SkYUVAPixmaps(const SkYUVAPixmapInfo&, sk_sp); + SkYUVAPixmaps(const SkYUVAInfo&, DataType, const SkPixmap[kMaxPlanes]); + + std::array fPlanes = {}; + sk_sp fData; + SkYUVAInfo fYUVAInfo; + DataType fDataType; +}; + +////////////////////////////////////////////////////////////////////////////// + +constexpr SkYUVAPixmapInfo::SupportedDataTypes SkYUVAPixmapInfo::SupportedDataTypes::All() { + using ULL = unsigned long long; // bitset cons. takes this. + ULL bits = 0; + for (ULL c = 1; c <= 4; ++c) { + for (ULL dt = 0; dt <= ULL(kDataTypeCnt); ++dt) { + if (DefaultColorTypeForDataType(static_cast(dt), + static_cast(c)) != kUnknown_SkColorType) { + bits |= ULL(1) << (dt + static_cast(kDataTypeCnt)*(c - 1)); + } + } + } + SupportedDataTypes combinations; + combinations.fDataTypeSupport = bits; + return combinations; +} + +constexpr bool SkYUVAPixmapInfo::SupportedDataTypes::supported(PlaneConfig config, + DataType type) const { + int n = SkYUVAInfo::NumPlanes(config); + for (int i = 0; i < n; ++i) { + auto c = static_cast(SkYUVAInfo::NumChannelsInPlane(config, i)); + SkASSERT(c >= 1 && c <= 4); + if (!fDataTypeSupport[static_cast(type) + + (c - 1)*static_cast(kDataTypeCnt)]) { + return false; + } + } + return true; +} + +constexpr SkColorType SkYUVAPixmapInfo::DefaultColorTypeForDataType(DataType dataType, + int numChannels) { + switch (numChannels) { + case 1: + switch (dataType) { + case DataType::kUnorm8: return kGray_8_SkColorType; + case DataType::kUnorm16: return kA16_unorm_SkColorType; + case DataType::kFloat16: return kA16_float_SkColorType; + case DataType::kUnorm10_Unorm2: return kUnknown_SkColorType; + } + break; + case 2: + switch (dataType) { + case DataType::kUnorm8: return kR8G8_unorm_SkColorType; + case DataType::kUnorm16: return kR16G16_unorm_SkColorType; + case DataType::kFloat16: return kR16G16_float_SkColorType; + case DataType::kUnorm10_Unorm2: return kUnknown_SkColorType; + } + break; + case 3: + // None of these are tightly packed. The intended use case is for interleaved YUVA + // planes where we're forcing opaqueness by ignoring the alpha values. + // There are "x" rather than "A" variants for Unorm8 and Unorm10_Unorm2 but we don't + // choose them because 1) there is no inherent advantage and 2) there is better support + // in the GPU backend for the "A" versions. + switch (dataType) { + case DataType::kUnorm8: return kRGBA_8888_SkColorType; + case DataType::kUnorm16: return kR16G16B16A16_unorm_SkColorType; + case DataType::kFloat16: return kRGBA_F16_SkColorType; + case DataType::kUnorm10_Unorm2: return kRGBA_1010102_SkColorType; + } + break; + case 4: + switch (dataType) { + case DataType::kUnorm8: return kRGBA_8888_SkColorType; + case DataType::kUnorm16: return kR16G16B16A16_unorm_SkColorType; + case DataType::kFloat16: return kRGBA_F16_SkColorType; + case DataType::kUnorm10_Unorm2: return kRGBA_1010102_SkColorType; + } + break; + } + return kUnknown_SkColorType; +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/docs/SkMultiPictureDocument.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/docs/SkMultiPictureDocument.h new file mode 100644 index 00000000000..5b8b8063b1c --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/docs/SkMultiPictureDocument.h @@ -0,0 +1,53 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkMultiPictureDocument_DEFINED +#define SkMultiPictureDocument_DEFINED + +#include "include/core/SkPicture.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkSize.h" +#include "include/core/SkTypes.h" + +#include + +class SkDocument; +class SkStreamSeekable; +class SkWStream; +struct SkDeserialProcs; +struct SkSerialProcs; + +struct SkDocumentPage { + sk_sp fPicture; + SkSize fSize; +}; + +namespace SkMultiPictureDocument { +/** + * Writes into a file format that is similar to SkPicture::serialize() + * Accepts a callback for endPage behavior + */ +SK_API sk_sp Make(SkWStream* dst, const SkSerialProcs* = nullptr, + std::function onEndPage = nullptr); + +/** + * Returns the number of pages in the SkMultiPictureDocument. + */ +SK_API int ReadPageCount(SkStreamSeekable* src); + +/** + * Read the SkMultiPictureDocument into the provided array of pages. + * dstArrayCount must equal SkMultiPictureDocumentReadPageCount(). + * Return false on error. + */ +SK_API bool Read(SkStreamSeekable* src, + SkDocumentPage* dstArray, + int dstArrayCount, + const SkDeserialProcs* = nullptr); +} // namespace SkMultiPictureDocument + +#endif // SkMultiPictureDocument_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/docs/SkPDFDocument.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/docs/SkPDFDocument.h new file mode 100644 index 00000000000..bf5fe9dd8e4 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/docs/SkPDFDocument.h @@ -0,0 +1,224 @@ +// Copyright 2018 Google LLC. +// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. +#ifndef SkPDFDocument_DEFINED +#define SkPDFDocument_DEFINED + +#include "include/core/SkDocument.h" +#include "include/core/SkMilestone.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkString.h" +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkNoncopyable.h" + +#include +#include +#include + +class SkCanvas; +class SkExecutor; +class SkPDFArray; +class SkPDFTagTree; +class SkWStream; + +#define SKPDF_STRING(X) SKPDF_STRING_IMPL(X) +#define SKPDF_STRING_IMPL(X) #X + +namespace SkPDF { + +/** Attributes for nodes in the PDF tree. */ +class SK_API AttributeList : SkNoncopyable { +public: + AttributeList(); + ~AttributeList(); + + // Each attribute must have an owner (e.g. "Layout", "List", "Table", etc) + // and an attribute name (e.g. "BBox", "RowSpan", etc.) from PDF32000_2008 14.8.5, + // and then a value of the proper type according to the spec. + void appendInt(const char* owner, const char* name, int value); + void appendFloat(const char* owner, const char* name, float value); + void appendName(const char* owner, const char* attrName, const char* value); + void appendFloatArray(const char* owner, + const char* name, + const std::vector& value); + void appendNodeIdArray(const char* owner, + const char* attrName, + const std::vector& nodeIds); + +private: + friend class ::SkPDFTagTree; + + std::unique_ptr fAttrs; +}; + +/** A node in a PDF structure tree, giving a semantic representation + of the content. Each node ID is associated with content + by passing the SkCanvas and node ID to SkPDF::SetNodeId() when drawing. + NodeIDs should be unique within each tree. +*/ +struct StructureElementNode { + SkString fTypeString; + std::vector> fChildVector; + int fNodeId = 0; + std::vector fAdditionalNodeIds; + AttributeList fAttributes; + SkString fAlt; + SkString fLang; +}; + +struct DateTime { + int16_t fTimeZoneMinutes; // The number of minutes that this + // is ahead of or behind UTC. + uint16_t fYear; //!< e.g. 2005 + uint8_t fMonth; //!< 1..12 + uint8_t fDayOfWeek; //!< 0..6, 0==Sunday + uint8_t fDay; //!< 1..31 + uint8_t fHour; //!< 0..23 + uint8_t fMinute; //!< 0..59 + uint8_t fSecond; //!< 0..59 + + void toISO8601(SkString* dst) const; +}; + +/** Optional metadata to be passed into the PDF factory function. +*/ +struct Metadata { + /** The document's title. + */ + SkString fTitle; + + /** The name of the person who created the document. + */ + SkString fAuthor; + + /** The subject of the document. + */ + SkString fSubject; + + /** Keywords associated with the document. Commas may be used to delineate + keywords within the string. + */ + SkString fKeywords; + + /** If the document was converted to PDF from another format, + the name of the conforming product that created the + original document from which it was converted. + */ + SkString fCreator; + + /** The product that is converting this document to PDF. + */ + SkString fProducer = SkString("Skia/PDF m" SKPDF_STRING(SK_MILESTONE)); + + /** The date and time the document was created. + The zero default value represents an unknown/unset time. + */ + DateTime fCreation = {0, 0, 0, 0, 0, 0, 0, 0}; + + /** The date and time the document was most recently modified. + The zero default value represents an unknown/unset time. + */ + DateTime fModified = {0, 0, 0, 0, 0, 0, 0, 0}; + + /** The natural language of the text in the PDF. If fLang is empty, the root + StructureElementNode::fLang will be used (if not empty). Text not in + this language should be marked with StructureElementNode::fLang. + */ + SkString fLang; + + /** The DPI (pixels-per-inch) at which features without native PDF support + will be rasterized (e.g. draw image with perspective, draw text with + perspective, ...) A larger DPI would create a PDF that reflects the + original intent with better fidelity, but it can make for larger PDF + files too, which would use more memory while rendering, and it would be + slower to be processed or sent online or to printer. + */ + SkScalar fRasterDPI = SK_ScalarDefaultRasterDPI; + + /** If true, include XMP metadata, a document UUID, and sRGB output intent + information. This adds length to the document and makes it + non-reproducable, but are necessary features for PDF/A-2b conformance + */ + bool fPDFA = false; + + /** Encoding quality controls the trade-off between size and quality. By + default this is set to 101 percent, which corresponds to lossless + encoding. If this value is set to a value <= 100, and the image is + opaque, it will be encoded (using JPEG) with that quality setting. + */ + int fEncodingQuality = 101; + + /** An optional tree of structured document tags that provide + a semantic representation of the content. The caller + should retain ownership. + */ + StructureElementNode* fStructureElementTreeRoot = nullptr; + + enum class Outline : int { + None = 0, + StructureElementHeaders = 1, + } fOutline = Outline::None; + + /** Executor to handle threaded work within PDF Backend. If this is nullptr, + then all work will be done serially on the main thread. To have worker + threads assist with various tasks, set this to a valid SkExecutor + instance. Currently used for executing Deflate algorithm in parallel. + + If set, the PDF output will be non-reproducible in the order and + internal numbering of objects, but should render the same. + + Experimental. + */ + SkExecutor* fExecutor = nullptr; + + /** PDF streams may be compressed to save space. + Use this to specify the desired compression vs time tradeoff. + */ + enum class CompressionLevel : int { + Default = -1, + None = 0, + LowButFast = 1, + Average = 6, + HighButSlow = 9, + } fCompressionLevel = CompressionLevel::Default; + + /** Preferred Subsetter. */ + enum Subsetter { + kHarfbuzz_Subsetter, + } fSubsetter = kHarfbuzz_Subsetter; +}; + +/** Associate a node ID with subsequent drawing commands in an + SkCanvas. The same node ID can appear in a StructureElementNode + in order to associate a document's structure element tree with + its content. + + A node ID of zero indicates no node ID. + + @param canvas The canvas used to draw to the PDF. + @param nodeId The node ID for subsequent drawing commands. +*/ +SK_API void SetNodeId(SkCanvas* dst, int nodeID); + +/** Create a PDF-backed document, writing the results into a SkWStream. + + PDF pages are sized in point units. 1 pt == 1/72 inch == 127/360 mm. + + @param stream A PDF document will be written to this stream. The document may write + to the stream at anytime during its lifetime, until either close() is + called or the document is deleted. + @param metadata a PDFmetadata object. Any fields may be left empty. + + @returns NULL if there is an error, otherwise a newly created PDF-backed SkDocument. +*/ +SK_API sk_sp MakeDocument(SkWStream* stream, const Metadata& metadata); + +static inline sk_sp MakeDocument(SkWStream* stream) { + return MakeDocument(stream, Metadata()); +} + +} // namespace SkPDF + +#undef SKPDF_STRING +#undef SKPDF_STRING_IMPL +#endif // SkPDFDocument_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/docs/SkXPSDocument.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/docs/SkXPSDocument.h new file mode 100644 index 00000000000..5cd0777c9b1 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/docs/SkXPSDocument.h @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkXPSDocument_DEFINED +#define SkXPSDocument_DEFINED + +#include "include/core/SkTypes.h" + +#ifdef SK_BUILD_FOR_WIN + +#include "include/core/SkDocument.h" + +struct IXpsOMObjectFactory; + +namespace SkXPS { + +SK_API sk_sp MakeDocument(SkWStream* stream, + IXpsOMObjectFactory* xpsFactory, + SkScalar dpi = SK_ScalarDefaultRasterDPI); + +} // namespace SkXPS +#endif // SK_BUILD_FOR_WIN +#endif // SkXPSDocument_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/Sk1DPathEffect.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/Sk1DPathEffect.h new file mode 100644 index 00000000000..fd05c52df7c --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/Sk1DPathEffect.h @@ -0,0 +1,40 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef Sk1DPathEffect_DEFINED +#define Sk1DPathEffect_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" + +class SkPath; +class SkPathEffect; + +class SK_API SkPath1DPathEffect { +public: + enum Style { + kTranslate_Style, // translate the shape to each position + kRotate_Style, // rotate the shape about its center + kMorph_Style, // transform each point, and turn lines into curves + + kLastEnum_Style = kMorph_Style, + }; + + /** Dash by replicating the specified path. + @param path The path to replicate (dash) + @param advance The space between instances of path + @param phase distance (mod advance) along path for its initial position + @param style how to transform path at each point (based on the current + position and tangent) + */ + static sk_sp Make(const SkPath& path, SkScalar advance, SkScalar phase, Style); + + static void RegisterFlattenables(); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/Sk2DPathEffect.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/Sk2DPathEffect.h new file mode 100644 index 00000000000..b8b3ba39817 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/Sk2DPathEffect.h @@ -0,0 +1,33 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef Sk2DPathEffect_DEFINED +#define Sk2DPathEffect_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" + +class SkMatrix; +class SkPath; +class SkPathEffect; + +class SK_API SkLine2DPathEffect { +public: + static sk_sp Make(SkScalar width, const SkMatrix& matrix); + + static void RegisterFlattenables(); +}; + +class SK_API SkPath2DPathEffect { +public: + static sk_sp Make(const SkMatrix& matrix, const SkPath& path); + + static void RegisterFlattenables(); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkBlenders.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkBlenders.h new file mode 100644 index 00000000000..7507071b056 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkBlenders.h @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkBlenders_DEFINED +#define SkBlenders_DEFINED + +#include "include/core/SkBlender.h" + +class SK_API SkBlenders { +public: + /** + * Create a blender that implements the following: + * k1 * src * dst + k2 * src + k3 * dst + k4 + * @param k1, k2, k3, k4 The four coefficients. + * @param enforcePMColor If true, the RGB channels will be clamped to the calculated alpha. + */ + static sk_sp Arithmetic(float k1, float k2, float k3, float k4, bool enforcePremul); + +private: + SkBlenders() = delete; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkBlurMaskFilter.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkBlurMaskFilter.h new file mode 100644 index 00000000000..1b9319869ed --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkBlurMaskFilter.h @@ -0,0 +1,35 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkBlurMaskFilter_DEFINED +#define SkBlurMaskFilter_DEFINED + +// we include this since our callers will need to at least be able to ref/unref +#include "include/core/SkBlurTypes.h" +#include "include/core/SkMaskFilter.h" +#include "include/core/SkRect.h" +#include "include/core/SkScalar.h" + +class SkRRect; + +class SK_API SkBlurMaskFilter { +public: +#ifdef SK_SUPPORT_LEGACY_EMBOSSMASKFILTER + /** Create an emboss maskfilter + @param blurSigma standard deviation of the Gaussian blur to apply + before applying lighting (e.g. 3) + @param direction array of 3 scalars [x, y, z] specifying the direction of the light source + @param ambient 0...1 amount of ambient light + @param specular coefficient for specular highlights (e.g. 8) + @return the emboss maskfilter + */ + static sk_sp MakeEmboss(SkScalar blurSigma, const SkScalar direction[3], + SkScalar ambient, SkScalar specular); +#endif +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkColorMatrix.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkColorMatrix.h new file mode 100644 index 00000000000..5092278f0de --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkColorMatrix.h @@ -0,0 +1,57 @@ +/* + * Copyright 2007 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkColorMatrix_DEFINED +#define SkColorMatrix_DEFINED + +#include "include/core/SkTypes.h" + +#include +#include + +enum SkYUVColorSpace : int; + +class SK_API SkColorMatrix { +public: + constexpr SkColorMatrix() : SkColorMatrix(1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0) {} + + constexpr SkColorMatrix(float m00, float m01, float m02, float m03, float m04, + float m10, float m11, float m12, float m13, float m14, + float m20, float m21, float m22, float m23, float m24, + float m30, float m31, float m32, float m33, float m34) + : fMat { m00, m01, m02, m03, m04, + m10, m11, m12, m13, m14, + m20, m21, m22, m23, m24, + m30, m31, m32, m33, m34 } {} + + static SkColorMatrix RGBtoYUV(SkYUVColorSpace); + static SkColorMatrix YUVtoRGB(SkYUVColorSpace); + + void setIdentity(); + void setScale(float rScale, float gScale, float bScale, float aScale = 1.0f); + + void postTranslate(float dr, float dg, float db, float da); + + void setConcat(const SkColorMatrix& a, const SkColorMatrix& b); + void preConcat(const SkColorMatrix& mat) { this->setConcat(*this, mat); } + void postConcat(const SkColorMatrix& mat) { this->setConcat(mat, *this); } + + void setSaturation(float sat); + + void setRowMajor(const float src[20]) { std::copy_n(src, 20, fMat.begin()); } + void getRowMajor(float dst[20]) const { std::copy_n(fMat.begin(), 20, dst); } + +private: + std::array fMat; + + friend class SkColorFilters; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkColorMatrixFilter.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkColorMatrixFilter.h new file mode 100644 index 00000000000..3e5337b0cf9 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkColorMatrixFilter.h @@ -0,0 +1,22 @@ +/* + * Copyright 2007 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkColorMatrixFilter_DEFINED +#define SkColorMatrixFilter_DEFINED + +#include "include/core/SkColorFilter.h" + +// (DEPRECATED) This factory function is deprecated. Please use the one in +// SkColorFilters (i.e., Lighting). +class SK_API SkColorMatrixFilter : public SkColorFilter { +public: + static sk_sp MakeLightingFilter(SkColor mul, SkColor add) { + return SkColorFilters::Lighting(mul, add); + } +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkCornerPathEffect.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkCornerPathEffect.h new file mode 100644 index 00000000000..7f7e7159f3f --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkCornerPathEffect.h @@ -0,0 +1,32 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkCornerPathEffect_DEFINED +#define SkCornerPathEffect_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" + +class SkPathEffect; + +/** \class SkCornerPathEffect + + SkCornerPathEffect is a subclass of SkPathEffect that can turn sharp corners + into various treatments (e.g. rounded corners) +*/ +class SK_API SkCornerPathEffect { +public: + /** radius must be > 0 to have an effect. It specifies the distance from each corner + that should be "rounded". + */ + static sk_sp Make(SkScalar radius); + + static void RegisterFlattenables(); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkDashPathEffect.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkDashPathEffect.h new file mode 100644 index 00000000000..f30064aa947 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkDashPathEffect.h @@ -0,0 +1,43 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkDashPathEffect_DEFINED +#define SkDashPathEffect_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" + +class SkPathEffect; + +class SK_API SkDashPathEffect { +public: + /** intervals: array containing an even number of entries (>=2), with + the even indices specifying the length of "on" intervals, and the odd + indices specifying the length of "off" intervals. This array will be + copied in Make, and can be disposed of freely after. + count: number of elements in the intervals array + phase: offset into the intervals array (mod the sum of all of the + intervals). + + For example: if intervals[] = {10, 20}, count = 2, and phase = 25, + this will set up a dashed path like so: + 5 pixels off + 10 pixels on + 20 pixels off + 10 pixels on + 20 pixels off + ... + A phase of -5, 25, 55, 85, etc. would all result in the same path, + because the sum of all the intervals is 30. + + Note: only affects stroked paths. + */ + static sk_sp Make(const SkScalar intervals[], int count, SkScalar phase); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkDiscretePathEffect.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkDiscretePathEffect.h new file mode 100644 index 00000000000..6054cbdc991 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkDiscretePathEffect.h @@ -0,0 +1,37 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkDiscretePathEffect_DEFINED +#define SkDiscretePathEffect_DEFINED + +#include "include/core/SkPathEffect.h" + +/** \class SkDiscretePathEffect + + This path effect chops a path into discrete segments, and randomly displaces them. +*/ +class SK_API SkDiscretePathEffect { +public: + /** Break the path into segments of segLength length, and randomly move the endpoints + away from the original path by a maximum of deviation. + Note: works on filled or framed paths + + @param seedAssist This is a caller-supplied seedAssist that modifies + the seed value that is used to randomize the path + segments' endpoints. If not supplied it defaults to 0, + in which case filtering a path multiple times will + result in the same set of segments (this is useful for + testing). If a caller does not want this behaviour + they can pass in a different seedAssist to get a + different set of path segments. + */ + static sk_sp Make(SkScalar segLength, SkScalar dev, uint32_t seedAssist = 0); + + static void RegisterFlattenables(); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkGradientShader.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkGradientShader.h new file mode 100644 index 00000000000..d725c2d8b12 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkGradientShader.h @@ -0,0 +1,354 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkGradientShader_DEFINED +#define SkGradientShader_DEFINED + +#include "include/core/SkColor.h" +#include "include/core/SkColorSpace.h" +#include "include/core/SkPoint.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkShader.h" // IWYU pragma: keep +#include "include/core/SkTileMode.h" +#include "include/private/base/SkAPI.h" + +#include +#include + +class SkMatrix; + +/** \class SkGradientShader + + SkGradientShader hosts factories for creating subclasses of SkShader that + render linear and radial gradients. In general, degenerate cases should not + produce surprising results, but there are several types of degeneracies: + + * A linear gradient made from the same two points. + * A radial gradient with a radius of zero. + * A sweep gradient where the start and end angle are the same. + * A two point conical gradient where the two centers and the two radii are + the same. + + For any degenerate gradient with a decal tile mode, it will draw empty since the interpolating + region is zero area and the outer region is discarded by the decal mode. + + For any degenerate gradient with a repeat or mirror tile mode, it will draw a solid color that + is the average gradient color, since infinitely many repetitions of the gradients will fill the + shape. + + For a clamped gradient, every type is well-defined at the limit except for linear gradients. The + radial gradient with zero radius becomes the last color. The sweep gradient draws the sector + from 0 to the provided angle with the first color, with a hardstop switching to the last color. + When the provided angle is 0, this is just the solid last color again. Similarly, the two point + conical gradient becomes a circle filled with the first color, sized to the provided radius, + with a hardstop switching to the last color. When the two radii are both zero, this is just the + solid last color. + + As a linear gradient approaches the degenerate case, its shader will approach the appearance of + two half planes, each filled by the first and last colors of the gradient. The planes will be + oriented perpendicular to the vector between the two defining points of the gradient. However, + once they become the same point, Skia cannot reconstruct what that expected orientation is. To + provide a stable and predictable color in this case, Skia just uses the last color as a solid + fill to be similar to many of the other degenerate gradients' behaviors in clamp mode. +*/ +class SK_API SkGradientShader { +public: + enum Flags { + /** By default gradients will interpolate their colors in unpremul space + * and then premultiply each of the results. By setting this flag, the + * gradients will premultiply their colors first, and then interpolate + * between them. + * example: https://fiddle.skia.org/c/@GradientShader_MakeLinear + */ + kInterpolateColorsInPremul_Flag = 1 << 0, + }; + + struct Interpolation { + enum class InPremul : bool { kNo = false, kYes = true }; + + enum class ColorSpace : uint8_t { + // Default Skia behavior: interpolate in the color space of the destination surface + kDestination, + + // https://www.w3.org/TR/css-color-4/#interpolation-space + kSRGBLinear, + kLab, + kOKLab, + // This is the same as kOKLab, except it has a simplified version of the CSS gamut + // mapping algorithm (https://www.w3.org/TR/css-color-4/#css-gamut-mapping) + // into Rec2020 space applied to it. + // Warning: This space is experimental and should not be used in production. + kOKLabGamutMap, + kLCH, + kOKLCH, + // This is the same as kOKLCH, except it has the same gamut mapping applied to it + // as kOKLabGamutMap does. + // Warning: This space is experimental and should not be used in production. + kOKLCHGamutMap, + kSRGB, + kHSL, + kHWB, + + kLastColorSpace = kHWB, + }; + static constexpr int kColorSpaceCount = static_cast(ColorSpace::kLastColorSpace) + 1; + + enum class HueMethod : uint8_t { + // https://www.w3.org/TR/css-color-4/#hue-interpolation + kShorter, + kLonger, + kIncreasing, + kDecreasing, + + kLastHueMethod = kDecreasing, + }; + static constexpr int kHueMethodCount = static_cast(HueMethod::kLastHueMethod) + 1; + + InPremul fInPremul = InPremul::kNo; + ColorSpace fColorSpace = ColorSpace::kDestination; + HueMethod fHueMethod = HueMethod::kShorter; // Only relevant for LCH, OKLCH, HSL, or HWB + + static Interpolation FromFlags(uint32_t flags) { + return {flags & kInterpolateColorsInPremul_Flag ? InPremul::kYes : InPremul::kNo, + ColorSpace::kDestination, + HueMethod::kShorter}; + } + }; + + /** Returns a shader that generates a linear gradient between the two specified points. +

+ @param pts The start and end points for the gradient. + @param colors The array[count] of colors, to be distributed between the two points + @param pos May be NULL. array[count] of SkScalars, or NULL, of the relative position of + each corresponding color in the colors array. If this is NULL, + the the colors are distributed evenly between the start and end point. + If this is not null, the values must lie between 0.0 and 1.0, and be + strictly increasing. If the first value is not 0.0, then an additional + color stop is added at position 0.0, with the same color as colors[0]. + If the the last value is not 1.0, then an additional color stop is added + at position 1.0, with the same color as colors[count - 1]. + @param count Must be >=2. The number of colors (and pos if not NULL) entries. + @param mode The tiling mode + + example: https://fiddle.skia.org/c/@GradientShader_MakeLinear + */ + static sk_sp MakeLinear(const SkPoint pts[2], + const SkColor colors[], const SkScalar pos[], int count, + SkTileMode mode, + uint32_t flags = 0, const SkMatrix* localMatrix = nullptr); + + /** Returns a shader that generates a linear gradient between the two specified points. +

+ @param pts The start and end points for the gradient. + @param colors The array[count] of colors, to be distributed between the two points + @param pos May be NULL. array[count] of SkScalars, or NULL, of the relative position of + each corresponding color in the colors array. If this is NULL, + the the colors are distributed evenly between the start and end point. + If this is not null, the values must lie between 0.0 and 1.0, and be + strictly increasing. If the first value is not 0.0, then an additional + color stop is added at position 0.0, with the same color as colors[0]. + If the the last value is not 1.0, then an additional color stop is added + at position 1.0, with the same color as colors[count - 1]. + @param count Must be >=2. The number of colors (and pos if not NULL) entries. + @param mode The tiling mode + + example: https://fiddle.skia.org/c/@GradientShader_MakeLinear + */ + static sk_sp MakeLinear(const SkPoint pts[2], + const SkColor4f colors[], sk_sp colorSpace, + const SkScalar pos[], int count, SkTileMode mode, + const Interpolation& interpolation, + const SkMatrix* localMatrix); + static sk_sp MakeLinear(const SkPoint pts[2], + const SkColor4f colors[], sk_sp colorSpace, + const SkScalar pos[], int count, SkTileMode mode, + uint32_t flags = 0, const SkMatrix* localMatrix = nullptr) { + return MakeLinear(pts, colors, std::move(colorSpace), pos, count, mode, + Interpolation::FromFlags(flags), localMatrix); + } + + /** Returns a shader that generates a radial gradient given the center and radius. +

+ @param center The center of the circle for this gradient + @param radius Must be positive. The radius of the circle for this gradient + @param colors The array[count] of colors, to be distributed between the center and edge of the circle + @param pos May be NULL. The array[count] of SkScalars, or NULL, of the relative position of + each corresponding color in the colors array. If this is NULL, + the the colors are distributed evenly between the center and edge of the circle. + If this is not null, the values must lie between 0.0 and 1.0, and be + strictly increasing. If the first value is not 0.0, then an additional + color stop is added at position 0.0, with the same color as colors[0]. + If the the last value is not 1.0, then an additional color stop is added + at position 1.0, with the same color as colors[count - 1]. + @param count Must be >= 2. The number of colors (and pos if not NULL) entries + @param mode The tiling mode + */ + static sk_sp MakeRadial(const SkPoint& center, SkScalar radius, + const SkColor colors[], const SkScalar pos[], int count, + SkTileMode mode, + uint32_t flags = 0, const SkMatrix* localMatrix = nullptr); + + /** Returns a shader that generates a radial gradient given the center and radius. +

+ @param center The center of the circle for this gradient + @param radius Must be positive. The radius of the circle for this gradient + @param colors The array[count] of colors, to be distributed between the center and edge of the circle + @param pos May be NULL. The array[count] of SkScalars, or NULL, of the relative position of + each corresponding color in the colors array. If this is NULL, + the the colors are distributed evenly between the center and edge of the circle. + If this is not null, the values must lie between 0.0 and 1.0, and be + strictly increasing. If the first value is not 0.0, then an additional + color stop is added at position 0.0, with the same color as colors[0]. + If the the last value is not 1.0, then an additional color stop is added + at position 1.0, with the same color as colors[count - 1]. + @param count Must be >= 2. The number of colors (and pos if not NULL) entries + @param mode The tiling mode + */ + static sk_sp MakeRadial(const SkPoint& center, SkScalar radius, + const SkColor4f colors[], sk_sp colorSpace, + const SkScalar pos[], int count, SkTileMode mode, + const Interpolation& interpolation, + const SkMatrix* localMatrix); + static sk_sp MakeRadial(const SkPoint& center, SkScalar radius, + const SkColor4f colors[], sk_sp colorSpace, + const SkScalar pos[], int count, SkTileMode mode, + uint32_t flags = 0, const SkMatrix* localMatrix = nullptr) { + return MakeRadial(center, radius, colors, std::move(colorSpace), pos, count, mode, + Interpolation::FromFlags(flags), localMatrix); + } + + /** + * Returns a shader that generates a conical gradient given two circles, or + * returns NULL if the inputs are invalid. The gradient interprets the + * two circles according to the following HTML spec. + * http://dev.w3.org/html5/2dcontext/#dom-context-2d-createradialgradient + */ + static sk_sp MakeTwoPointConical(const SkPoint& start, SkScalar startRadius, + const SkPoint& end, SkScalar endRadius, + const SkColor colors[], const SkScalar pos[], + int count, SkTileMode mode, + uint32_t flags = 0, + const SkMatrix* localMatrix = nullptr); + + /** + * Returns a shader that generates a conical gradient given two circles, or + * returns NULL if the inputs are invalid. The gradient interprets the + * two circles according to the following HTML spec. + * http://dev.w3.org/html5/2dcontext/#dom-context-2d-createradialgradient + */ + static sk_sp MakeTwoPointConical(const SkPoint& start, SkScalar startRadius, + const SkPoint& end, SkScalar endRadius, + const SkColor4f colors[], + sk_sp colorSpace, const SkScalar pos[], + int count, SkTileMode mode, + const Interpolation& interpolation, + const SkMatrix* localMatrix); + static sk_sp MakeTwoPointConical(const SkPoint& start, SkScalar startRadius, + const SkPoint& end, SkScalar endRadius, + const SkColor4f colors[], + sk_sp colorSpace, const SkScalar pos[], + int count, SkTileMode mode, + uint32_t flags = 0, + const SkMatrix* localMatrix = nullptr) { + return MakeTwoPointConical(start, startRadius, end, endRadius, colors, + std::move(colorSpace), pos, count, mode, + Interpolation::FromFlags(flags), localMatrix); + } + + /** Returns a shader that generates a sweep gradient given a center. + + The shader accepts negative angles and angles larger than 360, draws + between 0 and 360 degrees, similar to the CSS conic-gradient + semantics. 0 degrees means horizontal positive x axis. The start angle + must be less than the end angle, otherwise a null pointer is + returned. If color stops do not contain 0 and 1 but are within this + range, the respective outer color stop is repeated for 0 and 1. Color + stops less than 0 are clamped to 0, and greater than 1 are clamped to 1. +

+ @param cx The X coordinate of the center of the sweep + @param cx The Y coordinate of the center of the sweep + @param colors The array[count] of colors, to be distributed around the center, within + the gradient angle range. + @param pos May be NULL. The array[count] of SkScalars, or NULL, of the relative + position of each corresponding color in the colors array. If this is + NULL, then the colors are distributed evenly within the angular range. + If this is not null, the values must lie between 0.0 and 1.0, and be + strictly increasing. If the first value is not 0.0, then an additional + color stop is added at position 0.0, with the same color as colors[0]. + If the the last value is not 1.0, then an additional color stop is added + at position 1.0, with the same color as colors[count - 1]. + @param count Must be >= 2. The number of colors (and pos if not NULL) entries + @param mode Tiling mode: controls drawing outside of the gradient angular range. + @param startAngle Start of the angular range, corresponding to pos == 0. + @param endAngle End of the angular range, corresponding to pos == 1. + */ + static sk_sp MakeSweep(SkScalar cx, SkScalar cy, + const SkColor colors[], const SkScalar pos[], int count, + SkTileMode mode, + SkScalar startAngle, SkScalar endAngle, + uint32_t flags, const SkMatrix* localMatrix); + static sk_sp MakeSweep(SkScalar cx, SkScalar cy, + const SkColor colors[], const SkScalar pos[], int count, + uint32_t flags = 0, const SkMatrix* localMatrix = nullptr) { + return MakeSweep(cx, cy, colors, pos, count, SkTileMode::kClamp, 0, 360, flags, + localMatrix); + } + + /** Returns a shader that generates a sweep gradient given a center. + + The shader accepts negative angles and angles larger than 360, draws + between 0 and 360 degrees, similar to the CSS conic-gradient + semantics. 0 degrees means horizontal positive x axis. The start angle + must be less than the end angle, otherwise a null pointer is + returned. If color stops do not contain 0 and 1 but are within this + range, the respective outer color stop is repeated for 0 and 1. Color + stops less than 0 are clamped to 0, and greater than 1 are clamped to 1. +

+ @param cx The X coordinate of the center of the sweep + @param cx The Y coordinate of the center of the sweep + @param colors The array[count] of colors, to be distributed around the center, within + the gradient angle range. + @param pos May be NULL. The array[count] of SkScalars, or NULL, of the relative + position of each corresponding color in the colors array. If this is + NULL, then the colors are distributed evenly within the angular range. + If this is not null, the values must lie between 0.0 and 1.0, and be + strictly increasing. If the first value is not 0.0, then an additional + color stop is added at position 0.0, with the same color as colors[0]. + If the the last value is not 1.0, then an additional color stop is added + at position 1.0, with the same color as colors[count - 1]. + @param count Must be >= 2. The number of colors (and pos if not NULL) entries + @param mode Tiling mode: controls drawing outside of the gradient angular range. + @param startAngle Start of the angular range, corresponding to pos == 0. + @param endAngle End of the angular range, corresponding to pos == 1. + */ + static sk_sp MakeSweep(SkScalar cx, SkScalar cy, + const SkColor4f colors[], sk_sp colorSpace, + const SkScalar pos[], int count, + SkTileMode mode, + SkScalar startAngle, SkScalar endAngle, + const Interpolation& interpolation, + const SkMatrix* localMatrix); + static sk_sp MakeSweep(SkScalar cx, SkScalar cy, + const SkColor4f colors[], sk_sp colorSpace, + const SkScalar pos[], int count, + SkTileMode mode, + SkScalar startAngle, SkScalar endAngle, + uint32_t flags, const SkMatrix* localMatrix) { + return MakeSweep(cx, cy, colors, std::move(colorSpace), pos, count, mode, startAngle, + endAngle, Interpolation::FromFlags(flags), localMatrix); + } + static sk_sp MakeSweep(SkScalar cx, SkScalar cy, + const SkColor4f colors[], sk_sp colorSpace, + const SkScalar pos[], int count, + uint32_t flags = 0, const SkMatrix* localMatrix = nullptr) { + return MakeSweep(cx, cy, colors, std::move(colorSpace), pos, count, SkTileMode::kClamp, + 0, 360, flags, localMatrix); + } +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkHighContrastFilter.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkHighContrastFilter.h new file mode 100644 index 00000000000..6badf8486e0 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkHighContrastFilter.h @@ -0,0 +1,84 @@ +/* +* Copyright 2017 Google Inc. +* +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE file. +*/ + +#ifndef SkHighContrastFilter_DEFINED +#define SkHighContrastFilter_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" + +class SkColorFilter; + +/** + * Configuration struct for SkHighContrastFilter. + * + * Provides transformations to improve contrast for users with low vision. + */ +struct SkHighContrastConfig { + enum class InvertStyle { + kNoInvert, + kInvertBrightness, + kInvertLightness, + + kLast = kInvertLightness + }; + + SkHighContrastConfig() { + fGrayscale = false; + fInvertStyle = InvertStyle::kNoInvert; + fContrast = 0.0f; + } + + SkHighContrastConfig(bool grayscale, + InvertStyle invertStyle, + SkScalar contrast) + : fGrayscale(grayscale) + , fInvertStyle(invertStyle) + , fContrast(contrast) {} + + // Returns true if all of the fields are set within the valid range. + bool isValid() const { + return fInvertStyle >= InvertStyle::kNoInvert && + fInvertStyle <= InvertStyle::kInvertLightness && + fContrast >= -1.0 && + fContrast <= 1.0; + } + + // If true, the color will be converted to grayscale. + bool fGrayscale; + + // Whether to invert brightness, lightness, or neither. + InvertStyle fInvertStyle; + + // After grayscale and inverting, the contrast can be adjusted linearly. + // The valid range is -1.0 through 1.0, where 0.0 is no adjustment. + SkScalar fContrast; +}; + +/** + * Color filter that provides transformations to improve contrast + * for users with low vision. + * + * Applies the following transformations in this order. Each of these + * can be configured using SkHighContrastConfig. + * + * - Conversion to grayscale + * - Color inversion (either in RGB or HSL space) + * - Increasing the resulting contrast. + * + * Calling SkHighContrastFilter::Make will return nullptr if the config is + * not valid, e.g. if you try to call it with a contrast outside the range of + * -1.0 to 1.0. + */ + +struct SK_API SkHighContrastFilter { + // Returns the filter, or nullptr if the config is invalid. + static sk_sp Make(const SkHighContrastConfig& config); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkImageFilters.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkImageFilters.h new file mode 100644 index 00000000000..926896f9ed8 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkImageFilters.h @@ -0,0 +1,615 @@ +/* + * Copyright 2019 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkImageFilters_DEFINED +#define SkImageFilters_DEFINED + +#include "include/core/SkColor.h" +#include "include/core/SkImage.h" +#include "include/core/SkImageFilter.h" +#include "include/core/SkPicture.h" +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkShader.h" +#include "include/core/SkTileMode.h" +#include "include/core/SkTypes.h" + +#include +#include +#include +#include + +class SkBlender; +class SkColorFilter; +class SkMatrix; +class SkRuntimeShaderBuilder; +enum class SkBlendMode; +struct SkIPoint; +struct SkISize; +struct SkPoint3; +struct SkSamplingOptions; + +// A set of factory functions providing useful SkImageFilter effects. For image filters that take an +// input filter, providing nullptr means it will automatically use the dynamic source image. This +// source depends on how the filter is applied, but is either the contents of a saved layer when +// drawing with SkCanvas, or an explicit SkImage if using one of the SkImages::MakeWithFilter +// factories. +class SK_API SkImageFilters { +public: + // This is just a convenience type to allow passing SkIRects, SkRects, and optional pointers + // to those types as a crop rect for the image filter factories. It's not intended to be used + // directly. + struct CropRect : public std::optional { + CropRect() {} + // Intentionally not explicit so callers don't have to use this type but can use SkIRect or + // SkRect as desired. + CropRect(const SkIRect& crop) : std::optional(SkRect::Make(crop)) {} + CropRect(const SkRect& crop) : std::optional(crop) {} + CropRect(const std::optional& crop) : std::optional(crop) {} + CropRect(const std::nullopt_t&) : std::optional() {} + + // Backwards compatibility for when the APIs used to explicitly accept "const SkRect*" + CropRect(std::nullptr_t) {} + CropRect(const SkIRect* optionalCrop) { + if (optionalCrop) { + *this = SkRect::Make(*optionalCrop); + } + } + CropRect(const SkRect* optionalCrop) { + if (optionalCrop) { + *this = *optionalCrop; + } + } + + // std::optional doesn't define == when comparing to another optional... + bool operator==(const CropRect& o) const { + return this->has_value() == o.has_value() && + (!this->has_value() || this->value() == *o); + } + }; + + /** + * Create a filter that implements a custom blend mode. Each output pixel is the result of + * combining the corresponding background and foreground pixels using the 4 coefficients: + * k1 * foreground * background + k2 * foreground + k3 * background + k4 + * @param k1, k2, k3, k4 The four coefficients used to combine the foreground and background. + * @param enforcePMColor If true, the RGB channels will be clamped to the calculated alpha. + * @param background The background content, using the source bitmap when this is null. + * @param foreground The foreground content, using the source bitmap when this is null. + * @param cropRect Optional rectangle that crops the inputs and output. + */ + static sk_sp Arithmetic(SkScalar k1, SkScalar k2, SkScalar k3, SkScalar k4, + bool enforcePMColor, sk_sp background, + sk_sp foreground, + const CropRect& cropRect = {}); + + /** + * This filter takes an SkBlendMode and uses it to composite the two filters together. + * @param mode The blend mode that defines the compositing operation + * @param background The Dst pixels used in blending, if null the source bitmap is used. + * @param foreground The Src pixels used in blending, if null the source bitmap is used. + * @cropRect Optional rectangle to crop input and output. + */ + static sk_sp Blend(SkBlendMode mode, sk_sp background, + sk_sp foreground = nullptr, + const CropRect& cropRect = {}); + + /** + * This filter takes an SkBlendMode and uses it to composite the two filters together. + * @param blender The blender that defines the compositing operation + * @param background The Dst pixels used in blending, if null the source bitmap is used. + * @param foreground The Src pixels used in blending, if null the source bitmap is used. + * @cropRect Optional rectangle to crop input and output. + */ + static sk_sp Blend(sk_sp blender, sk_sp background, + sk_sp foreground = nullptr, + const CropRect& cropRect = {}); + + /** + * Create a filter that blurs its input by the separate X and Y sigmas. The provided tile mode + * is used when the blur kernel goes outside the input image. + * @param sigmaX The Gaussian sigma value for blurring along the X axis. + * @param sigmaY The Gaussian sigma value for blurring along the Y axis. + * @param tileMode The tile mode applied at edges . + * TODO (michaelludwig) - kMirror is not supported yet + * @param input The input filter that is blurred, uses source bitmap if this is null. + * @param cropRect Optional rectangle that crops the input and output. + */ + static sk_sp Blur(SkScalar sigmaX, SkScalar sigmaY, SkTileMode tileMode, + sk_sp input, const CropRect& cropRect = {}); + // As above, but defaults to the decal tile mode. + static sk_sp Blur(SkScalar sigmaX, SkScalar sigmaY, sk_sp input, + const CropRect& cropRect = {}) { + return Blur(sigmaX, sigmaY, SkTileMode::kDecal, std::move(input), cropRect); + } + + /** + * Create a filter that applies the color filter to the input filter results. + * @param cf The color filter that transforms the input image. + * @param input The input filter, or uses the source bitmap if this is null. + * @param cropRect Optional rectangle that crops the input and output. + */ + static sk_sp ColorFilter(sk_sp cf, sk_sp input, + const CropRect& cropRect = {}); + + /** + * Create a filter that composes 'inner' with 'outer', such that the results of 'inner' are + * treated as the source bitmap passed to 'outer', i.e. result = outer(inner(source)). + * @param outer The outer filter that evaluates the results of inner. + * @param inner The inner filter that produces the input to outer. + */ + static sk_sp Compose(sk_sp outer, sk_sp inner); + + /** + * Create a filter that applies a crop to the result of the 'input' filter. Pixels within the + * crop rectangle are unmodified from what 'input' produced. Pixels outside of crop match the + * provided SkTileMode (defaulting to kDecal). + * + * NOTE: The optional CropRect argument for many of the factories is equivalent to creating the + * filter without a CropRect and then wrapping it in ::Crop(rect, kDecal). Explicitly adding + * Crop filters lets you control their tiling and use different geometry for the input and the + * output of another filter. + * + * @param rect The cropping geometry + * @param tileMode The tilemode applied to pixels *outside* of 'crop' + * @param input The input filter that is cropped, uses source image if this is null + */ + static sk_sp Crop(const SkRect& rect, + SkTileMode tileMode, + sk_sp input); + static sk_sp Crop(const SkRect& rect, sk_sp input) { + return Crop(rect, SkTileMode::kDecal, std::move(input)); + } + + /** + * Create a filter that moves each pixel in its color input based on an (x,y) vector encoded + * in its displacement input filter. Two color components of the displacement image are + * mapped into a vector as scale * (color[xChannel], color[yChannel]), where the channel + * selectors are one of R, G, B, or A. + * @param xChannelSelector RGBA channel that encodes the x displacement per pixel. + * @param yChannelSelector RGBA channel that encodes the y displacement per pixel. + * @param scale Scale applied to displacement extracted from image. + * @param displacement The filter defining the displacement image, or null to use source. + * @param color The filter providing the color pixels to be displaced. If null, + * it will use the source. + * @param cropRect Optional rectangle that crops the color input and output. + */ + static sk_sp DisplacementMap(SkColorChannel xChannelSelector, + SkColorChannel yChannelSelector, + SkScalar scale, sk_sp displacement, + sk_sp color, + const CropRect& cropRect = {}); + + /** + * Create a filter that draws a drop shadow under the input content. This filter produces an + * image that includes the inputs' content. + * @param dx The X offset of the shadow. + * @param dy The Y offset of the shadow. + * @param sigmaX The blur radius for the shadow, along the X axis. + * @param sigmaY The blur radius for the shadow, along the Y axis. + * @param color The color of the drop shadow. + * @param input The input filter, or will use the source bitmap if this is null. + * @param cropRect Optional rectangle that crops the input and output. + */ + static sk_sp DropShadow(SkScalar dx, SkScalar dy, + SkScalar sigmaX, SkScalar sigmaY, + SkColor color, sk_sp input, + const CropRect& cropRect = {}); + /** + * Create a filter that renders a drop shadow, in exactly the same manner as ::DropShadow, + * except that the resulting image does not include the input content. This allows the shadow + * and input to be composed by a filter DAG in a more flexible manner. + * @param dx The X offset of the shadow. + * @param dy The Y offset of the shadow. + * @param sigmaX The blur radius for the shadow, along the X axis. + * @param sigmaY The blur radius for the shadow, along the Y axis. + * @param color The color of the drop shadow. + * @param input The input filter, or will use the source bitmap if this is null. + * @param cropRect Optional rectangle that crops the input and output. + */ + static sk_sp DropShadowOnly(SkScalar dx, SkScalar dy, + SkScalar sigmaX, SkScalar sigmaY, + SkColor color, sk_sp input, + const CropRect& cropRect = {}); + + /** + * Create a filter that always produces transparent black. + */ + static sk_sp Empty(); + + /** + * Create a filter that draws the 'srcRect' portion of image into 'dstRect' using the given + * filter quality. Similar to SkCanvas::drawImageRect. The returned image filter evaluates + * to transparent black if 'image' is null. + * + * @param image The image that is output by the filter, subset by 'srcRect'. + * @param srcRect The source pixels sampled into 'dstRect' + * @param dstRect The local rectangle to draw the image into. + * @param sampling The sampling to use when drawing the image. + */ + static sk_sp Image(sk_sp image, const SkRect& srcRect, + const SkRect& dstRect, const SkSamplingOptions& sampling); + + /** + * Create a filter that draws the image using the given sampling. + * Similar to SkCanvas::drawImage. The returned image filter evaluates to transparent black if + * 'image' is null. + * + * @param image The image that is output by the filter. + * @param sampling The sampling to use when drawing the image. + */ + static sk_sp Image(sk_sp image, const SkSamplingOptions& sampling) { + if (image) { + SkRect r = SkRect::Make(image->bounds()); + return Image(std::move(image), r, r, sampling); + } else { + return nullptr; + } + } + + /** + * Create a filter that fills 'lensBounds' with a magnification of the input. + * + * @param lensBounds The outer bounds of the magnifier effect + * @param zoomAmount The amount of magnification applied to the input image + * @param inset The size or width of the fish-eye distortion around the magnified content + * @param sampling The SkSamplingOptions applied to the input image when magnified + * @param input The input filter that is magnified; if null the source bitmap is used + * @param cropRect Optional rectangle that crops the input and output. + */ + static sk_sp Magnifier(const SkRect& lensBounds, + SkScalar zoomAmount, + SkScalar inset, + const SkSamplingOptions& sampling, + sk_sp input, + const CropRect& cropRect = {}); + + /** + * Create a filter that applies an NxM image processing kernel to the input image. This can be + * used to produce effects such as sharpening, blurring, edge detection, etc. + * @param kernelSize The kernel size in pixels, in each dimension (N by M). + * @param kernel The image processing kernel. Must contain N * M elements, in row order. + * @param gain A scale factor applied to each pixel after convolution. This can be + * used to normalize the kernel, if it does not already sum to 1. + * @param bias A bias factor added to each pixel after convolution. + * @param kernelOffset An offset applied to each pixel coordinate before convolution. + * This can be used to center the kernel over the image + * (e.g., a 3x3 kernel should have an offset of {1, 1}). + * @param tileMode How accesses outside the image are treated. + * TODO (michaelludwig) - kMirror is not supported yet + * @param convolveAlpha If true, all channels are convolved. If false, only the RGB channels + * are convolved, and alpha is copied from the source image. + * @param input The input image filter, if null the source bitmap is used instead. + * @param cropRect Optional rectangle to which the output processing will be limited. + */ + static sk_sp MatrixConvolution(const SkISize& kernelSize, + const SkScalar kernel[], SkScalar gain, + SkScalar bias, const SkIPoint& kernelOffset, + SkTileMode tileMode, bool convolveAlpha, + sk_sp input, + const CropRect& cropRect = {}); + + /** + * Create a filter that transforms the input image by 'matrix'. This matrix transforms the + * local space, which means it effectively happens prior to any transformation coming from the + * SkCanvas initiating the filtering. + * @param matrix The matrix to apply to the original content. + * @param sampling How the image will be sampled when it is transformed + * @param input The image filter to transform, or null to use the source image. + */ + static sk_sp MatrixTransform(const SkMatrix& matrix, + const SkSamplingOptions& sampling, + sk_sp input); + + /** + * Create a filter that merges the 'count' filters together by drawing their results in order + * with src-over blending. + * @param filters The input filter array to merge, which must have 'count' elements. Any null + * filter pointers will use the source bitmap instead. + * @param count The number of input filters to be merged. + * @param cropRect Optional rectangle that crops all input filters and the output. + */ + static sk_sp Merge(sk_sp* const filters, int count, + const CropRect& cropRect = {}); + /** + * Create a filter that merges the results of the two filters together with src-over blending. + * @param first The first input filter, or the source bitmap if this is null. + * @param second The second input filter, or the source bitmap if this null. + * @param cropRect Optional rectangle that crops the inputs and output. + */ + static sk_sp Merge(sk_sp first, sk_sp second, + const CropRect& cropRect = {}) { + sk_sp array[] = { std::move(first), std::move(second) }; + return Merge(array, 2, cropRect); + } + + /** + * Create a filter that offsets the input filter by the given vector. + * @param dx The x offset in local space that the image is shifted. + * @param dy The y offset in local space that the image is shifted. + * @param input The input that will be moved, if null the source bitmap is used instead. + * @param cropRect Optional rectangle to crop the input and output. + */ + static sk_sp Offset(SkScalar dx, SkScalar dy, sk_sp input, + const CropRect& cropRect = {}); + + /** + * Create a filter that produces the SkPicture as its output, clipped to both 'targetRect' and + * the picture's internal cull rect. + * + * If 'pic' is null, the returned image filter produces transparent black. + * + * @param pic The picture that is drawn for the filter output. + * @param targetRect The drawing region for the picture. + */ + static sk_sp Picture(sk_sp pic, const SkRect& targetRect); + // As above, but uses SkPicture::cullRect for the drawing region. + static sk_sp Picture(sk_sp pic) { + SkRect target = pic ? pic->cullRect() : SkRect::MakeEmpty(); + return Picture(std::move(pic), target); + } + + /** + * Create a filter that fills the output with the per-pixel evaluation of the SkShader produced + * by the SkRuntimeShaderBuilder. The shader is defined in the image filter's local coordinate + * system, so it will automatically be affected by SkCanvas' transform. + * + * This variant assumes that the runtime shader samples 'childShaderName' with the same input + * coordinate passed to to shader. + * + * This requires a GPU backend or SkSL to be compiled in. + * + * @param builder The builder used to produce the runtime shader, that will in turn + * fill the result image + * @param childShaderName The name of the child shader defined in the builder that will be + * bound to the input param (or the source image if the input param + * is null). If empty, the builder can have exactly one child shader, + * which automatically binds the input param. + * @param input The image filter that will be provided as input to the runtime + * shader. If null the implicit source image is used instead + */ + static sk_sp RuntimeShader(const SkRuntimeShaderBuilder& builder, + std::string_view childShaderName, + sk_sp input) { + return RuntimeShader(builder, /*sampleRadius=*/0.f, childShaderName, std::move(input)); + } + + /** + * As above, but 'sampleRadius' defines the sampling radius of 'childShaderName' relative to + * the runtime shader produced by 'builder'. If greater than 0, the coordinate passed to + * childShader.eval() will be up to 'sampleRadius' away (maximum absolute offset in 'x' or 'y') + * from the coordinate passed into the runtime shader. + * + * This allows Skia to provide sampleable values for the image filter without worrying about + * boundary conditions. + * + * This requires a GPU backend or SkSL to be compiled in. + */ + static sk_sp RuntimeShader(const SkRuntimeShaderBuilder& builder, + SkScalar sampleRadius, + std::string_view childShaderName, + sk_sp input); + + /** + * Create a filter that fills the output with the per-pixel evaluation of the SkShader produced + * by the SkRuntimeShaderBuilder. The shader is defined in the image filter's local coordinate + * system, so it will automatically be affected by SkCanvas' transform. + * + * This requires a GPU backend or SkSL to be compiled in. + * + * @param builder The builder used to produce the runtime shader, that will in turn + * fill the result image + * @param childShaderNames The names of the child shaders defined in the builder that will be + * bound to the input params (or the source image if the input param + * is null). If any name is null, or appears more than once, factory + * fails and returns nullptr. + * @param inputs The image filters that will be provided as input to the runtime + * shader. If any are null, the implicit source image is used instead. + * @param inputCount How many entries are present in 'childShaderNames' and 'inputs'. + */ + static sk_sp RuntimeShader(const SkRuntimeShaderBuilder& builder, + std::string_view childShaderNames[], + const sk_sp inputs[], + int inputCount) { + return RuntimeShader(builder, /*maxSampleRadius=*/0.f, childShaderNames, + inputs, inputCount); + } + + /** + * As above, but 'maxSampleRadius' defines the sampling limit on coordinates provided to all + * child shaders. Like the single-child variant with a sample radius, this can be used to + * inform Skia that the runtime shader guarantees that all dynamic children (defined in + * childShaderNames) will be evaluated with coordinates at most 'maxSampleRadius' away from the + * coordinate provided to the runtime shader itself. + * + * This requires a GPU backend or SkSL to be compiled in. + */ + static sk_sp RuntimeShader(const SkRuntimeShaderBuilder& builder, + SkScalar maxSampleRadius, + std::string_view childShaderNames[], + const sk_sp inputs[], + int inputCount); + + enum class Dither : bool { + kNo = false, + kYes = true + }; + + /** + * Create a filter that fills the output with the per-pixel evaluation of the SkShader. The + * shader is defined in the image filter's local coordinate system, so will automatically + * be affected by SkCanvas' transform. + * + * Like Image() and Picture(), this is a leaf filter that can be used to introduce inputs to + * a complex filter graph, but should generally be combined with a filter that as at least + * one null input to use the implicit source image. + * + * Returns an image filter that evaluates to transparent black if 'shader' is null. + * + * @param shader The shader that fills the result image + */ + static sk_sp Shader(sk_sp shader, const CropRect& cropRect = {}) { + return Shader(std::move(shader), Dither::kNo, cropRect); + } + static sk_sp Shader(sk_sp shader, Dither dither, + const CropRect& cropRect = {}); + + /** + * Create a tile image filter. + * @param src Defines the pixels to tile + * @param dst Defines the pixel region that the tiles will be drawn to + * @param input The input that will be tiled, if null the source bitmap is used instead. + */ + static sk_sp Tile(const SkRect& src, const SkRect& dst, + sk_sp input); + + // Morphology filter effects + + /** + * Create a filter that dilates each input pixel's channel values to the max value within the + * given radii along the x and y axes. + * @param radiusX The distance to dilate along the x axis to either side of each pixel. + * @param radiusY The distance to dilate along the y axis to either side of each pixel. + * @param input The image filter that is dilated, using source bitmap if this is null. + * @param cropRect Optional rectangle that crops the input and output. + */ + static sk_sp Dilate(SkScalar radiusX, SkScalar radiusY, + sk_sp input, + const CropRect& cropRect = {}); + + /** + * Create a filter that erodes each input pixel's channel values to the minimum channel value + * within the given radii along the x and y axes. + * @param radiusX The distance to erode along the x axis to either side of each pixel. + * @param radiusY The distance to erode along the y axis to either side of each pixel. + * @param input The image filter that is eroded, using source bitmap if this is null. + * @param cropRect Optional rectangle that crops the input and output. + */ + static sk_sp Erode(SkScalar radiusX, SkScalar radiusY, + sk_sp input, + const CropRect& cropRect = {}); + + // Lighting filter effects + + /** + * Create a filter that calculates the diffuse illumination from a distant light source, + * interpreting the alpha channel of the input as the height profile of the surface (to + * approximate normal vectors). + * @param direction The direction to the distance light. + * @param lightColor The color of the diffuse light source. + * @param surfaceScale Scale factor to transform from alpha values to physical height. + * @param kd Diffuse reflectance coefficient. + * @param input The input filter that defines surface normals (as alpha), or uses the + * source bitmap when null. + * @param cropRect Optional rectangle that crops the input and output. + */ + static sk_sp DistantLitDiffuse(const SkPoint3& direction, SkColor lightColor, + SkScalar surfaceScale, SkScalar kd, + sk_sp input, + const CropRect& cropRect = {}); + /** + * Create a filter that calculates the diffuse illumination from a point light source, using + * alpha channel of the input as the height profile of the surface (to approximate normal + * vectors). + * @param location The location of the point light. + * @param lightColor The color of the diffuse light source. + * @param surfaceScale Scale factor to transform from alpha values to physical height. + * @param kd Diffuse reflectance coefficient. + * @param input The input filter that defines surface normals (as alpha), or uses the + * source bitmap when null. + * @param cropRect Optional rectangle that crops the input and output. + */ + static sk_sp PointLitDiffuse(const SkPoint3& location, SkColor lightColor, + SkScalar surfaceScale, SkScalar kd, + sk_sp input, + const CropRect& cropRect = {}); + /** + * Create a filter that calculates the diffuse illumination from a spot light source, using + * alpha channel of the input as the height profile of the surface (to approximate normal + * vectors). The spot light is restricted to be within 'cutoffAngle' of the vector between + * the location and target. + * @param location The location of the spot light. + * @param target The location that the spot light is point towards + * @param falloffExponent Exponential falloff parameter for illumination outside of cutoffAngle + * @param cutoffAngle Maximum angle from lighting direction that receives full light + * @param lightColor The color of the diffuse light source. + * @param surfaceScale Scale factor to transform from alpha values to physical height. + * @param kd Diffuse reflectance coefficient. + * @param input The input filter that defines surface normals (as alpha), or uses the + * source bitmap when null. + * @param cropRect Optional rectangle that crops the input and output. + */ + static sk_sp SpotLitDiffuse(const SkPoint3& location, const SkPoint3& target, + SkScalar falloffExponent, SkScalar cutoffAngle, + SkColor lightColor, SkScalar surfaceScale, + SkScalar kd, sk_sp input, + const CropRect& cropRect = {}); + + /** + * Create a filter that calculates the specular illumination from a distant light source, + * interpreting the alpha channel of the input as the height profile of the surface (to + * approximate normal vectors). + * @param direction The direction to the distance light. + * @param lightColor The color of the specular light source. + * @param surfaceScale Scale factor to transform from alpha values to physical height. + * @param ks Specular reflectance coefficient. + * @param shininess The specular exponent determining how shiny the surface is. + * @param input The input filter that defines surface normals (as alpha), or uses the + * source bitmap when null. + * @param cropRect Optional rectangle that crops the input and output. + */ + static sk_sp DistantLitSpecular(const SkPoint3& direction, SkColor lightColor, + SkScalar surfaceScale, SkScalar ks, + SkScalar shininess, sk_sp input, + const CropRect& cropRect = {}); + /** + * Create a filter that calculates the specular illumination from a point light source, using + * alpha channel of the input as the height profile of the surface (to approximate normal + * vectors). + * @param location The location of the point light. + * @param lightColor The color of the specular light source. + * @param surfaceScale Scale factor to transform from alpha values to physical height. + * @param ks Specular reflectance coefficient. + * @param shininess The specular exponent determining how shiny the surface is. + * @param input The input filter that defines surface normals (as alpha), or uses the + * source bitmap when null. + * @param cropRect Optional rectangle that crops the input and output. + */ + static sk_sp PointLitSpecular(const SkPoint3& location, SkColor lightColor, + SkScalar surfaceScale, SkScalar ks, + SkScalar shininess, sk_sp input, + const CropRect& cropRect = {}); + /** + * Create a filter that calculates the specular illumination from a spot light source, using + * alpha channel of the input as the height profile of the surface (to approximate normal + * vectors). The spot light is restricted to be within 'cutoffAngle' of the vector between + * the location and target. + * @param location The location of the spot light. + * @param target The location that the spot light is point towards + * @param falloffExponent Exponential falloff parameter for illumination outside of cutoffAngle + * @param cutoffAngle Maximum angle from lighting direction that receives full light + * @param lightColor The color of the specular light source. + * @param surfaceScale Scale factor to transform from alpha values to physical height. + * @param ks Specular reflectance coefficient. + * @param shininess The specular exponent determining how shiny the surface is. + * @param input The input filter that defines surface normals (as alpha), or uses the + * source bitmap when null. + * @param cropRect Optional rectangle that crops the input and output. + */ + static sk_sp SpotLitSpecular(const SkPoint3& location, const SkPoint3& target, + SkScalar falloffExponent, SkScalar cutoffAngle, + SkColor lightColor, SkScalar surfaceScale, + SkScalar ks, SkScalar shininess, + sk_sp input, + const CropRect& cropRect = {}); + +private: + SkImageFilters() = delete; +}; + +#endif // SkImageFilters_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkLumaColorFilter.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkLumaColorFilter.h new file mode 100644 index 00000000000..41a9a45f3fb --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkLumaColorFilter.h @@ -0,0 +1,37 @@ +/* + * Copyright 2013 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkLumaColorFilter_DEFINED +#define SkLumaColorFilter_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" + +class SkColorFilter; + +/** + * SkLumaColorFilter multiplies the luma of its input into the alpha channel, + * and sets the red, green, and blue channels to zero. + * + * SkLumaColorFilter(r,g,b,a) = {0,0,0, a * luma(r,g,b)} + * + * This is similar to a luminanceToAlpha feColorMatrix, + * but note how this filter folds in the previous alpha, + * something an feColorMatrix cannot do. + * + * feColorMatrix(luminanceToAlpha; r,g,b,a) = {0,0,0, luma(r,g,b)} + * + * (Despite its name, an feColorMatrix using luminanceToAlpha does + * actually compute luma, a dot-product of gamma-encoded color channels, + * not luminance, a dot-product of linear color channels. So at least + * SkLumaColorFilter and feColorMatrix+luminanceToAlpha agree there.) + */ +struct SK_API SkLumaColorFilter { + static sk_sp Make(); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkOverdrawColorFilter.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkOverdrawColorFilter.h new file mode 100644 index 00000000000..5f1642483ae --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkOverdrawColorFilter.h @@ -0,0 +1,32 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "include/core/SkColor.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" + +class SkColorFilter; + +#ifndef SkOverdrawColorFilter_DEFINED +#define SkOverdrawColorFilter_DEFINED + +/** + * Uses the value in the src alpha channel to set the dst pixel. + * 0 -> colors[0] + * 1 -> colors[1] + * ... + * 5 (or larger) -> colors[5] + * + */ +class SK_API SkOverdrawColorFilter { +public: + static constexpr int kNumColors = 6; + + static sk_sp MakeWithSkColors(const SkColor[kNumColors]); +}; + +#endif // SkOverdrawColorFilter_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkPerlinNoiseShader.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkPerlinNoiseShader.h new file mode 100644 index 00000000000..7ef2f1fc509 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkPerlinNoiseShader.h @@ -0,0 +1,53 @@ +/* + * Copyright 2013 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPerlinNoiseShader_DEFINED +#define SkPerlinNoiseShader_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkShader.h" // IWYU pragma: keep +#include "include/private/base/SkAPI.h" + +struct SkISize; + +/** \class SkPerlinNoiseShader + + SkPerlinNoiseShader creates an image using the Perlin turbulence function. + + It can produce tileable noise if asked to stitch tiles and provided a tile size. + In order to fill a large area with repeating noise, set the stitchTiles flag to + true, and render exactly a single tile of noise. Without this flag, the result + will contain visible seams between tiles. + + The algorithm used is described here : + http://www.w3.org/TR/SVG/filters.html#feTurbulenceElement +*/ +namespace SkShaders { +/** + * This will construct Perlin noise of the given type (Fractal Noise or Turbulence). + * + * Both base frequencies (X and Y) have a usual range of (0..1) and must be non-negative. + * + * The number of octaves provided should be fairly small, with a limit of 255 enforced. + * Each octave doubles the frequency, so 10 octaves would produce noise from + * baseFrequency * 1, * 2, * 4, ..., * 512, which quickly yields insignificantly small + * periods and resembles regular unstructured noise rather than Perlin noise. + * + * If tileSize isn't NULL or an empty size, the tileSize parameter will be used to modify + * the frequencies so that the noise will be tileable for the given tile size. If tileSize + * is NULL or an empty size, the frequencies will be used as is without modification. + */ +SK_API sk_sp MakeFractalNoise(SkScalar baseFrequencyX, SkScalar baseFrequencyY, + int numOctaves, SkScalar seed, + const SkISize* tileSize = nullptr); +SK_API sk_sp MakeTurbulence(SkScalar baseFrequencyX, SkScalar baseFrequencyY, + int numOctaves, SkScalar seed, + const SkISize* tileSize = nullptr); +} // namespace SkShaders + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkRuntimeEffect.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkRuntimeEffect.h new file mode 100644 index 00000000000..d26e64bb40a --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkRuntimeEffect.h @@ -0,0 +1,517 @@ +/* + * Copyright 2019 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkRuntimeEffect_DEFINED +#define SkRuntimeEffect_DEFINED + +#include "include/core/SkBlender.h" // IWYU pragma: keep +#include "include/core/SkColorFilter.h" // IWYU pragma: keep +#include "include/core/SkData.h" +#include "include/core/SkFlattenable.h" +#include "include/core/SkMatrix.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkShader.h" +#include "include/core/SkSpan.h" +#include "include/core/SkString.h" +#include "include/core/SkTypes.h" +#include "include/private/SkSLSampleUsage.h" +#include "include/private/base/SkOnce.h" +#include "include/private/base/SkTemplates.h" +#include "include/private/base/SkTo.h" +#include "include/private/base/SkTypeTraits.h" +#include "include/sksl/SkSLDebugTrace.h" +#include "include/sksl/SkSLVersion.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct SkIPoint; + +namespace SkSL { +class DebugTracePriv; +class FunctionDefinition; +struct Program; +enum class ProgramKind : int8_t; +struct ProgramSettings; +} // namespace SkSL + +namespace SkSL::RP { +class Program; +} + +/* + * SkRuntimeEffect supports creating custom SkShader and SkColorFilter objects using Skia's SkSL + * shading language. + * + * NOTE: This API is experimental and subject to change. + */ +class SK_API SkRuntimeEffect : public SkRefCnt { +public: + // Reflected description of a uniform variable in the effect's SkSL + struct SK_API Uniform { + enum class Type { + kFloat, + kFloat2, + kFloat3, + kFloat4, + kFloat2x2, + kFloat3x3, + kFloat4x4, + kInt, + kInt2, + kInt3, + kInt4, + }; + + enum Flags { + // Uniform is declared as an array. 'count' contains array length. + kArray_Flag = 0x1, + + // Uniform is declared with layout(color). Colors should be supplied as unpremultiplied, + // extended-range (unclamped) sRGB (ie SkColor4f). The uniform will be automatically + // transformed to unpremultiplied extended-range working-space colors. + kColor_Flag = 0x2, + + // When used with SkMeshSpecification, indicates that the uniform is present in the + // vertex shader. Not used with SkRuntimeEffect. + kVertex_Flag = 0x4, + + // When used with SkMeshSpecification, indicates that the uniform is present in the + // fragment shader. Not used with SkRuntimeEffect. + kFragment_Flag = 0x8, + + // This flag indicates that the SkSL uniform uses a medium-precision type + // (i.e., `half` instead of `float`). + kHalfPrecision_Flag = 0x10, + }; + + std::string_view name; + size_t offset; + Type type; + int count; + uint32_t flags; + + bool isArray() const { return SkToBool(this->flags & kArray_Flag); } + bool isColor() const { return SkToBool(this->flags & kColor_Flag); } + size_t sizeInBytes() const; + }; + + // Reflected description of a uniform child (shader or colorFilter) in the effect's SkSL + enum class ChildType { + kShader, + kColorFilter, + kBlender, + }; + + struct Child { + std::string_view name; + ChildType type; + int index; + }; + + class Options { + public: + // For testing purposes, disables optimization and inlining. (Normally, Runtime Effects + // don't run the inliner directly, but they still get an inlining pass once they are + // painted.) + bool forceUnoptimized = false; + + private: + friend class SkRuntimeEffect; + friend class SkRuntimeEffectPriv; + + // This flag allows Runtime Effects to access Skia implementation details like sk_FragCoord + // and functions with private identifiers (e.g. $rgb_to_hsl). + bool allowPrivateAccess = false; + // When not 0, this field allows Skia to assign a stable key to a known runtime effect + uint32_t fStableKey = 0; + + // TODO(skia:11209) - Replace this with a promised SkCapabilities? + // This flag lifts the ES2 restrictions on Runtime Effects that are gated by the + // `strictES2Mode` check. Be aware that the software renderer and pipeline-stage effect are + // still largely ES3-unaware and can still fail or crash if post-ES2 features are used. + // This is only intended for use by tests and certain internally created effects. + SkSL::Version maxVersionAllowed = SkSL::Version::k100; + }; + + // If the effect is compiled successfully, `effect` will be non-null. + // Otherwise, `errorText` will contain the reason for failure. + struct Result { + sk_sp effect; + SkString errorText; + }; + + // MakeForColorFilter and MakeForShader verify that the SkSL code is valid for those stages of + // the Skia pipeline. In all of the signatures described below, color parameters and return + // values are flexible. They are listed as being 'vec4', but they can also be 'half4' or + // 'float4'. ('vec4' is an alias for 'float4'). + + // We can't use a default argument for `options` due to a bug in Clang. + // https://bugs.llvm.org/show_bug.cgi?id=36684 + + // Color filter SkSL requires an entry point that looks like: + // vec4 main(vec4 inColor) { ... } + static Result MakeForColorFilter(SkString sksl, const Options&); + static Result MakeForColorFilter(SkString sksl) { + return MakeForColorFilter(std::move(sksl), Options{}); + } + + // Shader SkSL requires an entry point that looks like: + // vec4 main(vec2 inCoords) { ... } + static Result MakeForShader(SkString sksl, const Options&); + static Result MakeForShader(SkString sksl) { + return MakeForShader(std::move(sksl), Options{}); + } + + // Blend SkSL requires an entry point that looks like: + // vec4 main(vec4 srcColor, vec4 dstColor) { ... } + static Result MakeForBlender(SkString sksl, const Options&); + static Result MakeForBlender(SkString sksl) { + return MakeForBlender(std::move(sksl), Options{}); + } + + // Object that allows passing a SkShader, SkColorFilter or SkBlender as a child + class SK_API ChildPtr { + public: + ChildPtr() = default; + ChildPtr(sk_sp s) : fChild(std::move(s)) {} + ChildPtr(sk_sp cf) : fChild(std::move(cf)) {} + ChildPtr(sk_sp b) : fChild(std::move(b)) {} + + // Asserts that the flattenable is either null, or one of the legal derived types + ChildPtr(sk_sp f); + + std::optional type() const; + + SkShader* shader() const; + SkColorFilter* colorFilter() const; + SkBlender* blender() const; + SkFlattenable* flattenable() const { return fChild.get(); } + + using sk_is_trivially_relocatable = std::true_type; + + private: + sk_sp fChild; + + static_assert(::sk_is_trivially_relocatable::value); + }; + + sk_sp makeShader(sk_sp uniforms, + sk_sp children[], + size_t childCount, + const SkMatrix* localMatrix = nullptr) const; + sk_sp makeShader(sk_sp uniforms, + SkSpan children, + const SkMatrix* localMatrix = nullptr) const; + + sk_sp makeColorFilter(sk_sp uniforms) const; + sk_sp makeColorFilter(sk_sp uniforms, + sk_sp children[], + size_t childCount) const; + sk_sp makeColorFilter(sk_sp uniforms, + SkSpan children) const; + + sk_sp makeBlender(sk_sp uniforms, + SkSpan children = {}) const; + + /** + * Creates a new Runtime Effect patterned after an already-existing one. The new shader behaves + * like the original, but also creates a debug trace of its execution at the requested + * coordinate. After painting with this shader, the associated DebugTrace object will contain a + * shader execution trace. Call `writeTrace` on the debug trace object to generate a full trace + * suitable for a debugger, or call `dump` to emit a human-readable trace. + * + * Debug traces are only supported on a raster (non-GPU) canvas. + + * Debug traces are currently only supported on shaders. Color filter and blender tracing is a + * work-in-progress. + */ + struct TracedShader { + sk_sp shader; + sk_sp debugTrace; + }; + static TracedShader MakeTraced(sk_sp shader, const SkIPoint& traceCoord); + + // Returns the SkSL source of the runtime effect shader. + const std::string& source() const; + + // Combined size of all 'uniform' variables. When calling makeColorFilter or makeShader, + // provide an SkData of this size, containing values for all of those variables. + size_t uniformSize() const; + + SkSpan uniforms() const { return SkSpan(fUniforms); } + SkSpan children() const { return SkSpan(fChildren); } + + // Returns pointer to the named uniform variable's description, or nullptr if not found + const Uniform* findUniform(std::string_view name) const; + + // Returns pointer to the named child's description, or nullptr if not found + const Child* findChild(std::string_view name) const; + + // Allows the runtime effect type to be identified. + bool allowShader() const { return (fFlags & kAllowShader_Flag); } + bool allowColorFilter() const { return (fFlags & kAllowColorFilter_Flag); } + bool allowBlender() const { return (fFlags & kAllowBlender_Flag); } + + static void RegisterFlattenables(); + ~SkRuntimeEffect() override; + +private: + enum Flags { + kUsesSampleCoords_Flag = 0x001, + kAllowColorFilter_Flag = 0x002, + kAllowShader_Flag = 0x004, + kAllowBlender_Flag = 0x008, + kSamplesOutsideMain_Flag = 0x010, + kUsesColorTransform_Flag = 0x020, + kAlwaysOpaque_Flag = 0x040, + kAlphaUnchanged_Flag = 0x080, + kDisableOptimization_Flag = 0x100, + }; + + SkRuntimeEffect(std::unique_ptr baseProgram, + const Options& options, + const SkSL::FunctionDefinition& main, + std::vector&& uniforms, + std::vector&& children, + std::vector&& sampleUsages, + uint32_t flags); + + sk_sp makeUnoptimizedClone(); + + static Result MakeFromSource(SkString sksl, const Options& options, SkSL::ProgramKind kind); + + static Result MakeInternal(std::unique_ptr program, + const Options& options, + SkSL::ProgramKind kind); + + static SkSL::ProgramSettings MakeSettings(const Options& options); + + uint32_t hash() const { return fHash; } + bool usesSampleCoords() const { return (fFlags & kUsesSampleCoords_Flag); } + bool samplesOutsideMain() const { return (fFlags & kSamplesOutsideMain_Flag); } + bool usesColorTransform() const { return (fFlags & kUsesColorTransform_Flag); } + bool alwaysOpaque() const { return (fFlags & kAlwaysOpaque_Flag); } + bool isAlphaUnchanged() const { return (fFlags & kAlphaUnchanged_Flag); } + + const SkSL::RP::Program* getRPProgram(SkSL::DebugTracePriv* debugTrace) const; + + friend class GrSkSLFP; // usesColorTransform + friend class SkRuntimeShader; // fBaseProgram, fMain, fSampleUsages, getRPProgram() + friend class SkRuntimeBlender; // + friend class SkRuntimeColorFilter; // + + friend class SkRuntimeEffectPriv; + + uint32_t fHash; + uint32_t fStableKey; + + std::unique_ptr fBaseProgram; + std::unique_ptr fRPProgram; + mutable SkOnce fCompileRPProgramOnce; + const SkSL::FunctionDefinition& fMain; + std::vector fUniforms; + std::vector fChildren; + std::vector fSampleUsages; + + uint32_t fFlags; // Flags +}; + +/** Base class for SkRuntimeShaderBuilder, defined below. */ +class SkRuntimeEffectBuilder { +public: + struct BuilderUniform { + // Copy 'val' to this variable. No type conversion is performed - 'val' must be same + // size as expected by the effect. Information about the variable can be queried by + // looking at fVar. If the size is incorrect, no copy will be performed, and debug + // builds will abort. If this is the result of querying a missing variable, fVar will + // be nullptr, and assigning will also do nothing (and abort in debug builds). + template + std::enable_if_t::value, BuilderUniform&> operator=( + const T& val) { + if (!fVar) { + SkDEBUGFAIL("Assigning to missing variable"); + } else if (sizeof(val) != fVar->sizeInBytes()) { + SkDEBUGFAIL("Incorrect value size"); + } else { + memcpy(SkTAddOffset(fOwner->writableUniformData(), fVar->offset), + &val, sizeof(val)); + } + return *this; + } + + BuilderUniform& operator=(const SkMatrix& val) { + if (!fVar) { + SkDEBUGFAIL("Assigning to missing variable"); + } else if (fVar->sizeInBytes() != 9 * sizeof(float)) { + SkDEBUGFAIL("Incorrect value size"); + } else { + float* data = SkTAddOffset(fOwner->writableUniformData(), + (ptrdiff_t)fVar->offset); + data[0] = val.get(0); data[1] = val.get(3); data[2] = val.get(6); + data[3] = val.get(1); data[4] = val.get(4); data[5] = val.get(7); + data[6] = val.get(2); data[7] = val.get(5); data[8] = val.get(8); + } + return *this; + } + + template + bool set(const T val[], const int count) { + static_assert(std::is_trivially_copyable::value, "Value must be trivial copyable"); + if (!fVar) { + SkDEBUGFAIL("Assigning to missing variable"); + return false; + } else if (sizeof(T) * count != fVar->sizeInBytes()) { + SkDEBUGFAIL("Incorrect value size"); + return false; + } else { + memcpy(SkTAddOffset(fOwner->writableUniformData(), fVar->offset), + val, sizeof(T) * count); + } + return true; + } + + SkRuntimeEffectBuilder* fOwner; + const SkRuntimeEffect::Uniform* fVar; // nullptr if the variable was not found + }; + + struct BuilderChild { + template BuilderChild& operator=(sk_sp val) { + if (!fChild) { + SkDEBUGFAIL("Assigning to missing child"); + } else { + fOwner->fChildren[(size_t)fChild->index] = std::move(val); + } + return *this; + } + + BuilderChild& operator=(std::nullptr_t) { + if (!fChild) { + SkDEBUGFAIL("Assigning to missing child"); + } else { + fOwner->fChildren[(size_t)fChild->index] = SkRuntimeEffect::ChildPtr{}; + } + return *this; + } + + SkRuntimeEffectBuilder* fOwner; + const SkRuntimeEffect::Child* fChild; // nullptr if the child was not found + }; + + const SkRuntimeEffect* effect() const { return fEffect.get(); } + + BuilderUniform uniform(std::string_view name) { return { this, fEffect->findUniform(name) }; } + BuilderChild child(std::string_view name) { return { this, fEffect->findChild(name) }; } + + // Get access to the collated uniforms and children (in the order expected by APIs like + // makeShader on the effect): + sk_sp uniforms() const { return fUniforms; } + SkSpan children() const { return fChildren; } + +protected: + SkRuntimeEffectBuilder() = delete; + explicit SkRuntimeEffectBuilder(sk_sp effect) + : fEffect(std::move(effect)) + , fUniforms(SkData::MakeZeroInitialized(fEffect->uniformSize())) + , fChildren(fEffect->children().size()) {} + explicit SkRuntimeEffectBuilder(sk_sp effect, sk_sp uniforms) + : fEffect(std::move(effect)) + , fUniforms(std::move(uniforms)) + , fChildren(fEffect->children().size()) {} + + SkRuntimeEffectBuilder(SkRuntimeEffectBuilder&&) = default; + SkRuntimeEffectBuilder(const SkRuntimeEffectBuilder&) = default; + + SkRuntimeEffectBuilder& operator=(SkRuntimeEffectBuilder&&) = delete; + SkRuntimeEffectBuilder& operator=(const SkRuntimeEffectBuilder&) = delete; + +private: + void* writableUniformData() { + if (!fUniforms->unique()) { + fUniforms = SkData::MakeWithCopy(fUniforms->data(), fUniforms->size()); + } + return fUniforms->writable_data(); + } + + sk_sp fEffect; + sk_sp fUniforms; + std::vector fChildren; +}; + +/** + * SkRuntimeShaderBuilder is a utility to simplify creating SkShader objects from SkRuntimeEffects. + * + * NOTE: Like SkRuntimeEffect, this API is experimental and subject to change! + * + * Given an SkRuntimeEffect, the SkRuntimeShaderBuilder manages creating an input data block and + * provides named access to the 'uniform' variables in that block, as well as named access + * to a list of child shader slots. Usage: + * + * sk_sp effect = ...; + * SkRuntimeShaderBuilder builder(effect); + * builder.uniform("some_uniform_float") = 3.14f; + * builder.uniform("some_uniform_matrix") = SkM44::Rotate(...); + * builder.child("some_child_effect") = mySkImage->makeShader(...); + * ... + * sk_sp shader = builder.makeShader(nullptr, false); + * + * Note that SkRuntimeShaderBuilder is built entirely on the public API of SkRuntimeEffect, + * so can be used as-is or serve as inspiration for other interfaces or binding techniques. + */ +class SK_API SkRuntimeShaderBuilder : public SkRuntimeEffectBuilder { +public: + explicit SkRuntimeShaderBuilder(sk_sp); + // This is currently required by Android Framework but may go away if that dependency + // can be removed. + SkRuntimeShaderBuilder(const SkRuntimeShaderBuilder&) = default; + ~SkRuntimeShaderBuilder(); + + sk_sp makeShader(const SkMatrix* localMatrix = nullptr) const; + +private: + explicit SkRuntimeShaderBuilder(sk_sp effect, sk_sp uniforms) + : SkRuntimeEffectBuilder(std::move(effect), std::move(uniforms)) {} + + friend class SkRuntimeImageFilter; +}; + +/** + * SkRuntimeColorFilterBuilder makes it easy to setup and assign uniforms to runtime color filters. + */ +class SK_API SkRuntimeColorFilterBuilder : public SkRuntimeEffectBuilder { +public: + explicit SkRuntimeColorFilterBuilder(sk_sp); + ~SkRuntimeColorFilterBuilder(); + + SkRuntimeColorFilterBuilder(const SkRuntimeColorFilterBuilder&) = delete; + SkRuntimeColorFilterBuilder& operator=(const SkRuntimeColorFilterBuilder&) = delete; + + sk_sp makeColorFilter() const; +}; + +/** + * SkRuntimeBlendBuilder is a utility to simplify creation and uniform setup of runtime blenders. + */ +class SK_API SkRuntimeBlendBuilder : public SkRuntimeEffectBuilder { +public: + explicit SkRuntimeBlendBuilder(sk_sp); + ~SkRuntimeBlendBuilder(); + + SkRuntimeBlendBuilder(const SkRuntimeBlendBuilder&) = delete; + SkRuntimeBlendBuilder& operator=(const SkRuntimeBlendBuilder&) = delete; + + sk_sp makeBlender() const; +}; + +#endif // SkRuntimeEffect_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkShaderMaskFilter.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkShaderMaskFilter.h new file mode 100644 index 00000000000..b0c269e8691 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkShaderMaskFilter.h @@ -0,0 +1,28 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkShaderMaskFilter_DEFINED +#define SkShaderMaskFilter_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" + +class SkMaskFilter; +class SkShader; + +// (DEPRECATED) This factory function is deprecated. ShaderMaskFilters will be deleted entirely +// in an upcoming Skia release. +class SK_API SkShaderMaskFilter { +public: + static sk_sp Make(sk_sp shader); + +private: + static void RegisterFlattenables(); + friend class SkFlattenable; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkTableMaskFilter.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkTableMaskFilter.h new file mode 100644 index 00000000000..937037afa80 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkTableMaskFilter.h @@ -0,0 +1,43 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTableMaskFilter_DEFINED +#define SkTableMaskFilter_DEFINED + +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" + +#include + +class SkMaskFilter; + +/** \class SkTableMaskFilter + + Applies a table lookup on each of the alpha values in the mask. + Helper methods create some common tables (e.g. gamma, clipping) + */ +// (DEPRECATED) These factory functions are deprecated. The TableMaskFilter will be +// removed entirely in an upcoming release of Skia. +class SK_API SkTableMaskFilter { +public: + /** Utility that sets the gamma table + */ + static void MakeGammaTable(uint8_t table[256], SkScalar gamma); + + /** Utility that creates a clipping table: clamps values below min to 0 + and above max to 255, and rescales the remaining into 0..255 + */ + static void MakeClipTable(uint8_t table[256], uint8_t min, uint8_t max); + + static SkMaskFilter* Create(const uint8_t table[256]); + static SkMaskFilter* CreateGamma(SkScalar gamma); + static SkMaskFilter* CreateClip(uint8_t min, uint8_t max); + + SkTableMaskFilter() = delete; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkTrimPathEffect.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkTrimPathEffect.h new file mode 100644 index 00000000000..3e6fb7c3424 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/effects/SkTrimPathEffect.h @@ -0,0 +1,45 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTrimPathEffect_DEFINED +#define SkTrimPathEffect_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" + +class SkPathEffect; + +class SK_API SkTrimPathEffect { +public: + enum class Mode { + kNormal, // return the subset path [start,stop] + kInverted, // return the complement/subset paths [0,start] + [stop,1] + }; + + /** + * Take start and stop "t" values (values between 0...1), and return a path that is that + * subset of the original path. + * + * e.g. + * Make(0.5, 1.0) --> return the 2nd half of the path + * Make(0.33333, 0.66667) --> return the middle third of the path + * + * The trim values apply to the entire path, so if it contains several contours, all of them + * are including in the calculation. + * + * startT and stopT must be 0..1 inclusive. If they are outside of that interval, they will + * be pinned to the nearest legal value. If either is NaN, null will be returned. + * + * Note: for Mode::kNormal, this will return one (logical) segment (even if it is spread + * across multiple contours). For Mode::kInverted, this will return 2 logical + * segments: stopT..1 and 0...startT, in this order. + */ + static sk_sp Make(SkScalar startT, SkScalar stopT, Mode = Mode::kNormal); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkEncoder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkEncoder.h new file mode 100644 index 00000000000..8f76e8016c8 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkEncoder.h @@ -0,0 +1,63 @@ +/* + * Copyright 2017 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkEncoder_DEFINED +#define SkEncoder_DEFINED + +#include "include/core/SkPixmap.h" +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkNoncopyable.h" +#include "include/private/base/SkTemplates.h" + +#include +#include + +class SK_API SkEncoder : SkNoncopyable { +public: + /** + * A single frame to be encoded into an animated image. + * + * If a frame does not fit in the canvas size, this is an error. + * TODO(skia:13705): Add offsets when we have support for an encoder that supports using + * offsets. + */ + struct SK_API Frame { + /** + * Pixmap of the frame. + */ + SkPixmap pixmap; + /** + * Duration of the frame in millseconds. + */ + int duration; + }; + + /** + * Encode |numRows| rows of input. If the caller requests more rows than are remaining + * in the src, this will encode all of the remaining rows. |numRows| must be greater + * than zero. + */ + bool encodeRows(int numRows); + + virtual ~SkEncoder() {} + +protected: + + virtual bool onEncodeRows(int numRows) = 0; + + SkEncoder(const SkPixmap& src, size_t storageBytes) + : fSrc(src) + , fCurrRow(0) + , fStorage(storageBytes) + {} + + const SkPixmap& fSrc; + int fCurrRow; + skia_private::AutoTMalloc fStorage; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkICC.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkICC.h new file mode 100644 index 00000000000..b14836b2ab6 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkICC.h @@ -0,0 +1,36 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkICC_DEFINED +#define SkICC_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +#include + +class SkData; +struct skcms_ICCProfile; +struct skcms_Matrix3x3; +struct skcms_TransferFunction; + +SK_API sk_sp SkWriteICCProfile(const skcms_TransferFunction&, + const skcms_Matrix3x3& toXYZD50); + +SK_API sk_sp SkWriteICCProfile(const skcms_ICCProfile*, const char* description); + +// Utility function for populating the grid_16 member of skcms_A2B and skcms_B2A +// structures. This converts a point in XYZD50 to its representation in grid_16_lab. +// It will write 6 bytes. The behavior of this function matches how skcms will decode +// values, but might not match the specification, see https://crbug.com/skia/13807. +SK_API void SkICCFloatXYZD50ToGrid16Lab(const float* float_xyz, uint8_t* grid16_lab); + +// Utility function for popluating the table_16 member of skcms_Curve structure. +// This converts a float to its representation in table_16. It will write 2 bytes. +SK_API void SkICCFloatToTable16(const float f, uint8_t* table_16); + +#endif//SkICC_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkJpegEncoder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkJpegEncoder.h new file mode 100644 index 00000000000..f7e8effa7fa --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkJpegEncoder.h @@ -0,0 +1,128 @@ +/* + * Copyright 2017 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkJpegEncoder_DEFINED +#define SkJpegEncoder_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +#include + +class SkColorSpace; +class SkData; +class SkEncoder; +class SkPixmap; +class SkWStream; +class SkImage; +class GrDirectContext; +class SkYUVAPixmaps; +struct skcms_ICCProfile; + +namespace SkJpegEncoder { + +enum class AlphaOption { + kIgnore, + kBlendOnBlack, +}; + +enum class Downsample { + /** + * Reduction by a factor of two in both the horizontal and vertical directions. + */ + k420, + + /** + * Reduction by a factor of two in the horizontal direction. + */ + k422, + + /** + * No downsampling. + */ + k444, +}; + +struct Options { + /** + * |fQuality| must be in [0, 100] where 0 corresponds to the lowest quality. + */ + int fQuality = 100; + + /** + * Choose the downsampling factor for the U and V components. This is only + * meaningful if the |src| is not kGray, since kGray will not be encoded as YUV. + * This is ignored in favor of |src|'s subsampling when |src| is an SkYUVAPixmaps. + * + * Our default value matches the libjpeg-turbo default. + */ + Downsample fDownsample = Downsample::k420; + + /** + * Jpegs must be opaque. This instructs the encoder on how to handle input + * images with alpha. + * + * The default is to ignore the alpha channel and treat the image as opaque. + * Another option is to blend the pixels onto a black background before encoding. + * In the second case, the encoder supports linear or legacy blending. + */ + AlphaOption fAlphaOption = AlphaOption::kIgnore; + + /** + * Optional XMP metadata. + */ + const SkData* xmpMetadata = nullptr; + + /** + * An optional ICC profile to override the default behavior. + * + * The default behavior is to generate an ICC profile using a primary matrix and + * analytic transfer function. If the color space of |src| cannot be represented + * in this way (e.g, it is HLG or PQ), then no profile will be embedded. + */ + const skcms_ICCProfile* fICCProfile = nullptr; + const char* fICCProfileDescription = nullptr; +}; + +/** + * Encode the |src| pixels to the |dst| stream. + * |options| may be used to control the encoding behavior. + * + * Returns true on success. Returns false on an invalid or unsupported |src|. + */ +SK_API bool Encode(SkWStream* dst, const SkPixmap& src, const Options& options); +SK_API bool Encode(SkWStream* dst, + const SkYUVAPixmaps& src, + const SkColorSpace* srcColorSpace, + const Options& options); + +/** +* Encode the provided image and return the resulting bytes. If the image was created as +* a texture-backed image on a GPU context, that |ctx| must be provided so the pixels +* can be read before being encoded. For raster-backed images, |ctx| can be nullptr. +* |options| may be used to control the encoding behavior. +* +* Returns nullptr if the pixels could not be read or encoding otherwise fails. +*/ +SK_API sk_sp Encode(GrDirectContext* ctx, const SkImage* img, const Options& options); + +/** + * Create a jpeg encoder that will encode the |src| pixels to the |dst| stream. + * |options| may be used to control the encoding behavior. + * + * |dst| is unowned but must remain valid for the lifetime of the object. + * + * This returns nullptr on an invalid or unsupported |src|. + */ +SK_API std::unique_ptr Make(SkWStream* dst, const SkPixmap& src, const Options& options); +SK_API std::unique_ptr Make(SkWStream* dst, + const SkYUVAPixmaps& src, + const SkColorSpace* srcColorSpace, + const Options& options); +} // namespace SkJpegEncoder + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkPngEncoder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkPngEncoder.h new file mode 100644 index 00000000000..b26befa323e --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkPngEncoder.h @@ -0,0 +1,117 @@ +/* + * Copyright 2017 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPngEncoder_DEFINED +#define SkPngEncoder_DEFINED + +#include "include/core/SkDataTable.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +// TODO(kjlubick) update clients to directly include this +#include "include/encode/SkEncoder.h" // IWYU pragma: keep + +#include + +class GrDirectContext; +class SkData; +class SkImage; +class SkPixmap; +class SkWStream; +struct skcms_ICCProfile; + +namespace SkPngEncoder { + +enum class FilterFlag : int { + kZero = 0x00, + kNone = 0x08, + kSub = 0x10, + kUp = 0x20, + kAvg = 0x40, + kPaeth = 0x80, + kAll = kNone | kSub | kUp | kAvg | kPaeth, +}; + +inline FilterFlag operator|(FilterFlag x, FilterFlag y) { return (FilterFlag)((int)x | (int)y); } + +struct Options { + /** + * Selects which filtering strategies to use. + * + * If a single filter is chosen, libpng will use that filter for every row. + * + * If multiple filters are chosen, libpng will use a heuristic to guess which filter + * will encode smallest, then apply that filter. This happens on a per row basis, + * different rows can use different filters. + * + * Using a single filter (or less filters) is typically faster. Trying all of the + * filters may help minimize the output file size. + * + * Our default value matches libpng's default. + */ + FilterFlag fFilterFlags = FilterFlag::kAll; + + /** + * Must be in [0, 9] where 9 corresponds to maximal compression. This value is passed + * directly to zlib. 0 is a special case to skip zlib entirely, creating dramatically + * larger pngs. + * + * Our default value matches libpng's default. + */ + int fZLibLevel = 6; + + /** + * Represents comments in the tEXt ancillary chunk of the png. + * The 2i-th entry is the keyword for the i-th comment, + * and the (2i + 1)-th entry is the text for the i-th comment. + */ + sk_sp fComments; + + /** + * An optional ICC profile to override the default behavior. + * + * The default behavior is to generate an ICC profile using a primary matrix and + * analytic transfer function. If the color space of |src| cannot be represented + * in this way (e.g, it is HLG or PQ), then no profile will be embedded. + */ + const skcms_ICCProfile* fICCProfile = nullptr; + const char* fICCProfileDescription = nullptr; +}; + +/** + * Encode the |src| pixels to the |dst| stream. + * |options| may be used to control the encoding behavior. + * + * Returns true on success. Returns false on an invalid or unsupported |src|. + */ +SK_API bool Encode(SkWStream* dst, const SkPixmap& src, const Options& options); + +/** +* Encode the provided image and return the resulting bytes. If the image was created as +* a texture-backed image on a GPU context, that |ctx| must be provided so the pixels +* can be read before being encoded. For raster-backed images, |ctx| can be nullptr. +* |options| may be used to control the encoding behavior. +* +* Returns nullptr if the pixels could not be read or encoding otherwise fails. +*/ +SK_API sk_sp Encode(GrDirectContext* ctx, const SkImage* img, const Options& options); + +/** + * Create a png encoder that will encode the |src| pixels to the |dst| stream. + * |options| may be used to control the encoding behavior. + * + * The primary use of this is incremental encoding of the pixels. + * + * |dst| is unowned but must remain valid for the lifetime of the object. + * + * This returns nullptr on an invalid or unsupported |src|. + */ +SK_API std::unique_ptr Make(SkWStream* dst, const SkPixmap& src, const Options& options); + +} // namespace SkPngEncoder + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkWebpEncoder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkWebpEncoder.h new file mode 100644 index 00000000000..fe11044e738 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/encode/SkWebpEncoder.h @@ -0,0 +1,92 @@ +/* + * Copyright 2017 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkWebpEncoder_DEFINED +#define SkWebpEncoder_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkSpan.h" // IWYU pragma: keep +#include "include/encode/SkEncoder.h" +#include "include/private/base/SkAPI.h" + +class SkPixmap; +class SkWStream; +class SkData; +class GrDirectContext; +class SkImage; +struct skcms_ICCProfile; + +namespace SkWebpEncoder { + +enum class Compression { + kLossy, + kLossless, +}; + +struct SK_API Options { + /** + * |fCompression| determines whether we will use webp lossy or lossless compression. + * + * |fQuality| must be in [0.0f, 100.0f]. + * If |fCompression| is kLossy, |fQuality| corresponds to the visual quality of the + * encoding. Decreasing the quality will result in a smaller encoded image. + * If |fCompression| is kLossless, |fQuality| corresponds to the amount of effort + * put into the encoding. Lower values will compress faster into larger files, + * while larger values will compress slower into smaller files. + * + * This scheme is designed to match the libwebp API. + */ + Compression fCompression = Compression::kLossy; + float fQuality = 100.0f; + + /** + * An optional ICC profile to override the default behavior. + * + * The default behavior is to generate an ICC profile using a primary matrix and + * analytic transfer function. If the color space of |src| cannot be represented + * in this way (e.g, it is HLG or PQ), then no profile will be embedded. + */ + const skcms_ICCProfile* fICCProfile = nullptr; + const char* fICCProfileDescription = nullptr; +}; + +/** + * Encode the |src| pixels to the |dst| stream. + * |options| may be used to control the encoding behavior. + * + * Returns true on success. Returns false on an invalid or unsupported |src|. + */ +SK_API bool Encode(SkWStream* dst, const SkPixmap& src, const Options& options); + +/** +* Encode the provided image and return the resulting bytes. If the image was created as +* a texture-backed image on a GPU context, that |ctx| must be provided so the pixels +* can be read before being encoded. For raster-backed images, |ctx| can be nullptr. +* |options| may be used to control the encoding behavior. +* +* Returns nullptr if the pixels could not be read or encoding otherwise fails. +*/ +SK_API sk_sp Encode(GrDirectContext* ctx, const SkImage* img, const Options& options); + +/** + * Encode the |src| frames to the |dst| stream. + * |options| may be used to control the encoding behavior. + * + * The size of the first frame will be used as the canvas size. If any other frame does + * not match the canvas size, this is an error. + * + * Returns true on success. Returns false on an invalid or unsupported |src|. + * + * Note: libwebp API also supports set background color, loop limit and customize + * lossy/lossless for each frame. These could be added later as needed. + */ +SK_API bool EncodeAnimated(SkWStream* dst, + SkSpan src, + const Options& options); +} // namespace SkWebpEncoder + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/pathops/SkPathOps.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/pathops/SkPathOps.h new file mode 100644 index 00000000000..47d2b3118fb --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/pathops/SkPathOps.h @@ -0,0 +1,113 @@ +/* + * Copyright 2012 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkPathOps_DEFINED +#define SkPathOps_DEFINED + +#include "include/core/SkPath.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkTArray.h" +#include "include/private/base/SkTDArray.h" + +struct SkRect; + + +// FIXME: move everything below into the SkPath class +/** + * The logical operations that can be performed when combining two paths. + */ +enum SkPathOp { + kDifference_SkPathOp, //!< subtract the op path from the first path + kIntersect_SkPathOp, //!< intersect the two paths + kUnion_SkPathOp, //!< union (inclusive-or) the two paths + kXOR_SkPathOp, //!< exclusive-or the two paths + kReverseDifference_SkPathOp, //!< subtract the first path from the op path +}; + +/** Set this path to the result of applying the Op to this path and the + specified path: this = (this op operand). + The resulting path will be constructed from non-overlapping contours. + The curve order is reduced where possible so that cubics may be turned + into quadratics, and quadratics maybe turned into lines. + + Returns true if operation was able to produce a result; + otherwise, result is unmodified. + + @param one The first operand (for difference, the minuend) + @param two The second operand (for difference, the subtrahend) + @param op The operator to apply. + @param result The product of the operands. The result may be one of the + inputs. + @return True if the operation succeeded. + */ +bool SK_API Op(const SkPath& one, const SkPath& two, SkPathOp op, SkPath* result); + +/** Set this path to a set of non-overlapping contours that describe the + same area as the original path. + The curve order is reduced where possible so that cubics may + be turned into quadratics, and quadratics maybe turned into lines. + + Returns true if operation was able to produce a result; + otherwise, result is unmodified. + + @param path The path to simplify. + @param result The simplified path. The result may be the input. + @return True if simplification succeeded. + */ +bool SK_API Simplify(const SkPath& path, SkPath* result); + +/** Set the resulting rectangle to the tight bounds of the path. + + @param path The path measured. + @param result The tight bounds of the path. + @return True if the bounds could be computed. + */ +bool SK_API TightBounds(const SkPath& path, SkRect* result); + +/** Set the result with fill type winding to area equivalent to path. + Returns true if successful. Does not detect if path contains contours which + contain self-crossings or cross other contours; in these cases, may return + true even though result does not fill same area as path. + + Returns true if operation was able to produce a result; + otherwise, result is unmodified. The result may be the input. + + @param path The path typically with fill type set to even odd. + @param result The equivalent path with fill type set to winding. + @return True if winding path was set. + */ +bool SK_API AsWinding(const SkPath& path, SkPath* result); + +/** Perform a series of path operations, optimized for unioning many paths together. + */ +class SK_API SkOpBuilder { +public: + /** Add one or more paths and their operand. The builder is empty before the first + path is added, so the result of a single add is (emptyPath OP path). + + @param path The second operand. + @param _operator The operator to apply to the existing and supplied paths. + */ + void add(const SkPath& path, SkPathOp _operator); + + /** Computes the sum of all paths and operands, and resets the builder to its + initial state. + + @param result The product of the operands. + @return True if the operation succeeded. + */ + bool resolve(SkPath* result); + +private: + skia_private::TArray fPathRefs; + SkTDArray fOps; + + static bool FixWinding(SkPath* path); + static void ReversePath(SkPath* path); + void reset(); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkCFObject.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkCFObject.h new file mode 100644 index 00000000000..4e56d06d06c --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkCFObject.h @@ -0,0 +1,180 @@ +/* + * Copyright 2019 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkCFObject_DEFINED +#define SkCFObject_DEFINED + +#ifdef __APPLE__ + +#include "include/core/SkTypes.h" + +#include // std::nullptr_t + +#import + +/** + * Wrapper class for managing lifetime of CoreFoundation objects. It will call + * CFRetain and CFRelease appropriately on creation, assignment, and deletion. + * Based on sk_sp<>. + */ +template static inline T SkCFSafeRetain(T obj) { + if (obj) { + CFRetain(obj); + } + return obj; +} + +template static inline void SkCFSafeRelease(T obj) { + if (obj) { + CFRelease(obj); + } +} + +template class sk_cfp { +public: + using element_type = T; + + constexpr sk_cfp() {} + constexpr sk_cfp(std::nullptr_t) {} + + /** + * Shares the underlying object by calling CFRetain(), so that both the argument and the newly + * created sk_cfp both have a reference to it. + */ + sk_cfp(const sk_cfp& that) : fObject(SkCFSafeRetain(that.get())) {} + + /** + * Move the underlying object from the argument to the newly created sk_cfp. Afterwards only + * the new sk_cfp will have a reference to the object, and the argument will point to null. + * No call to CFRetain() or CFRelease() will be made. + */ + sk_cfp(sk_cfp&& that) : fObject(that.release()) {} + + /** + * Adopt the bare object into the newly created sk_cfp. + * No call to CFRetain() or CFRelease() will be made. + */ + explicit sk_cfp(T obj) { + fObject = obj; + } + + /** + * Calls CFRelease() on the underlying object pointer. + */ + ~sk_cfp() { + SkCFSafeRelease(fObject); + SkDEBUGCODE(fObject = nil); + } + + sk_cfp& operator=(std::nullptr_t) { this->reset(); return *this; } + + /** + * Shares the underlying object referenced by the argument by calling CFRetain() on it. If this + * sk_cfp previously had a reference to an object (i.e. not null) it will call CFRelease() + * on that object. + */ + sk_cfp& operator=(const sk_cfp& that) { + if (this != &that) { + this->reset(SkCFSafeRetain(that.get())); + } + return *this; + } + + /** + * Move the underlying object from the argument to the sk_cfp. If the sk_cfp + * previously held a reference to another object, CFRelease() will be called on that object. + * No call to CFRetain() will be made. + */ + sk_cfp& operator=(sk_cfp&& that) { + this->reset(that.release()); + return *this; + } + + explicit operator bool() const { return this->get() != nil; } + + T get() const { return fObject; } + T operator*() const { + SkASSERT(fObject); + return fObject; + } + + /** + * Adopt the new object, and call CFRelease() on any previously held object (if not null). + * No call to CFRetain() will be made. + */ + void reset(T object = nil) { + // Need to unref after assigning, see + // http://wg21.cmeerw.net/lwg/issue998 + // http://wg21.cmeerw.net/lwg/issue2262 + T oldObject = fObject; + fObject = object; + SkCFSafeRelease(oldObject); + } + + /** + * Shares the new object by calling CFRetain() on it. If this sk_cfp previously had a + * reference to an object (i.e. not null) it will call CFRelease() on that object. + */ + void retain(T object) { + if (fObject != object) { + this->reset(SkCFSafeRetain(object)); + } + } + + /** + * Return the original object, and set the internal object to nullptr. + * The caller must assume ownership of the object, and manage its reference count directly. + * No call to CFRelease() will be made. + */ + [[nodiscard]] T release() { + T obj = fObject; + fObject = nil; + return obj; + } + +private: + T fObject = nil; +}; + +template inline bool operator==(const sk_cfp& a, + const sk_cfp& b) { + return a.get() == b.get(); +} +template inline bool operator==(const sk_cfp& a, + std::nullptr_t) { + return !a; +} +template inline bool operator==(std::nullptr_t, + const sk_cfp& b) { + return !b; +} + +template inline bool operator!=(const sk_cfp& a, + const sk_cfp& b) { + return a.get() != b.get(); +} +template inline bool operator!=(const sk_cfp& a, + std::nullptr_t) { + return static_cast(a); +} +template inline bool operator!=(std::nullptr_t, + const sk_cfp& b) { + return static_cast(b); +} + +/* + * Returns a sk_cfp wrapping the provided object AND calls retain on it (if not null). + * + * This is different than the semantics of the constructor for sk_cfp, which just wraps the + * object, effectively "adopting" it. + */ +template sk_cfp sk_ret_cfp(T obj) { + return sk_cfp(SkCFSafeRetain(obj)); +} + +#endif // __APPLE__ +#endif // SkCFObject_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontConfigInterface.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontConfigInterface.h new file mode 100644 index 00000000000..f8fdca53f9a --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontConfigInterface.h @@ -0,0 +1,112 @@ +/* + * Copyright 2013 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFontConfigInterface_DEFINED +#define SkFontConfigInterface_DEFINED + +#include "include/core/SkFontStyle.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkStream.h" +#include "include/core/SkTypeface.h" + +class SkFontMgr; + +/** + * \class SkFontConfigInterface + * + * A simple interface for remotable font management. + * The global instance can be found with RefGlobal(). + */ +class SK_API SkFontConfigInterface : public SkRefCnt { +public: + + /** + * Returns the global SkFontConfigInterface instance. If it is not + * nullptr, calls ref() on it. The caller must balance this with a call to + * unref(). The default SkFontConfigInterface is the result of calling + * GetSingletonDirectInterface. + */ + static sk_sp RefGlobal(); + + /** + * Replace the current global instance with the specified one. + */ + static void SetGlobal(sk_sp fc); + + /** + * This should be treated as private to the impl of SkFontConfigInterface. + * Callers should not change or expect any particular values. It is meant + * to be a union of possible storage types to aid the impl. + */ + struct FontIdentity { + FontIdentity() : fID(0), fTTCIndex(0) {} + + bool operator==(const FontIdentity& other) const { + return fID == other.fID && + fTTCIndex == other.fTTCIndex && + fString == other.fString; + } + bool operator!=(const FontIdentity& other) const { + return !(*this == other); + } + + uint32_t fID; + int32_t fTTCIndex; + SkString fString; + SkFontStyle fStyle; + + // If buffer is NULL, just return the number of bytes that would have + // been written. Will pad contents to a multiple of 4. + size_t writeToMemory(void* buffer = nullptr) const; + + // Recreate from a flattened buffer, returning the number of bytes read. + size_t readFromMemory(const void* buffer, size_t length); + }; + + /** + * Given a familyName and style, find the best match. + * + * If a match is found, return true and set its outFontIdentifier. + * If outFamilyName is not null, assign the found familyName to it + * (which may differ from the requested familyName). + * If outStyle is not null, assign the found style to it + * (which may differ from the requested style). + * + * If a match is not found, return false, and ignore all out parameters. + */ + virtual bool matchFamilyName(const char familyName[], + SkFontStyle requested, + FontIdentity* outFontIdentifier, + SkString* outFamilyName, + SkFontStyle* outStyle) = 0; + + /** + * Given a FontRef, open a stream to access its data, or return null + * if the FontRef's data is not available. The caller is responsible for + * deleting the stream when it is done accessing the data. + */ + virtual SkStreamAsset* openStream(const FontIdentity&) = 0; + + /** + * Return an SkTypeface for the given FontIdentity. + * + * The default implementation simply returns a new typeface built using data obtained from + * openStream() using the provided SkFontMgr, but derived classes may implement more + * complex caching schemes. + */ + virtual sk_sp makeTypeface(const FontIdentity& identity, sk_sp mgr); + + /** + * Return a singleton instance of a direct subclass that calls into + * libfontconfig. This does not affect the refcnt of the returned instance. + */ + static SkFontConfigInterface* GetSingletonDirectInterface(); + + using INHERITED = SkRefCnt; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_FontConfigInterface.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_FontConfigInterface.h new file mode 100644 index 00000000000..05771257d25 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_FontConfigInterface.h @@ -0,0 +1,20 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFontMgr_FontConfigInterface_DEFINED +#define SkFontMgr_FontConfigInterface_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" + +class SkFontMgr; +class SkFontConfigInterface; + +/** Creates a SkFontMgr which wraps a SkFontConfigInterface. */ +SK_API sk_sp SkFontMgr_New_FCI(sk_sp fci); + +#endif // #ifndef SkFontMgr_FontConfigInterface_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_Fontations.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_Fontations.h new file mode 100644 index 00000000000..c5b546c367d --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_Fontations.h @@ -0,0 +1,20 @@ +/* + * Copyright 2024 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFontMgr_fontations_DEFINED +#define SkFontMgr_fontations_DEFINED + +#include "include/core/SkRefCnt.h" + +class SkFontMgr; + +/** Create a font manager instantiating fonts using the Rust Fontations backend. + * This font manager does not support matching fonts, only instantiation. + */ +SK_API sk_sp SkFontMgr_New_Fontations_Empty(); + +#endif // #ifndef SkFontMgr_fontations_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_android.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_android.h new file mode 100644 index 00000000000..cb3bea29a2e --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_android.h @@ -0,0 +1,50 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFontMgr_android_DEFINED +#define SkFontMgr_android_DEFINED + +#include "include/core/SkRefCnt.h" + +class SkFontMgr; +class SkFontScanner; + +struct SkFontMgr_Android_CustomFonts { + /** When specifying custom fonts, indicates how to use system fonts. */ + enum SystemFontUse { + kOnlyCustom, /** Use only custom fonts. NDK compliant. */ + kPreferCustom, /** Use custom fonts before system fonts. */ + kPreferSystem /** Use system fonts before custom fonts. */ + }; + /** Whether or not to use system fonts. */ + SystemFontUse fSystemFontUse; + + /** Base path to resolve relative font file names. If a directory, should end with '/'. */ + const char* fBasePath; + + /** Optional custom configuration file to use. */ + const char* fFontsXml; + + /** Optional custom configuration file for fonts which provide fallback. + * In the new style (version > 21) fontsXml format is used, this should be NULL. + */ + const char* fFallbackFontsXml; + + /** Optional custom flag. If set to true the SkFontMgr will acquire all requisite + * system IO resources on initialization. + */ + bool fIsolated; +}; + +/** Create a font manager for Android. If 'custom' is NULL, use only system fonts. */ + +// Deprecated +SK_API sk_sp SkFontMgr_New_Android(const SkFontMgr_Android_CustomFonts* custom); + +SK_API sk_sp SkFontMgr_New_Android(const SkFontMgr_Android_CustomFonts* custom, + std::unique_ptr scanner); +#endif // SkFontMgr_android_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_data.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_data.h new file mode 100644 index 00000000000..6a22365af43 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_data.h @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkFontMgr_data_DEFINED +#define SkFontMgr_data_DEFINED + +#include "include/core/SkData.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkSpan.h" +#include "include/core/SkTypes.h" + +class SkFontMgr; + +/** Create a custom font manager which wraps a collection of SkData-stored fonts. + * This font manager uses FreeType for rendering. + */ +SK_API sk_sp SkFontMgr_New_Custom_Data(SkSpan>); + +#endif // SkFontMgr_data_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_directory.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_directory.h new file mode 100644 index 00000000000..b1a60fb4dac --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_directory.h @@ -0,0 +1,21 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFontMgr_directory_DEFINED +#define SkFontMgr_directory_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" + +class SkFontMgr; + +/** Create a custom font manager which scans a given directory for font files. + * This font manager uses FreeType for rendering. + */ +SK_API sk_sp SkFontMgr_New_Custom_Directory(const char* dir); + +#endif // SkFontMgr_directory_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_empty.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_empty.h new file mode 100644 index 00000000000..e5756421d0c --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_empty.h @@ -0,0 +1,21 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFontMgr_empty_DEFINED +#define SkFontMgr_empty_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" + +class SkFontMgr; + +/** Create a custom font manager that contains no built-in fonts. + * This font manager uses FreeType for rendering. + */ +SK_API sk_sp SkFontMgr_New_Custom_Empty(); + +#endif // SkFontMgr_empty_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_fontconfig.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_fontconfig.h new file mode 100644 index 00000000000..4b2bb2d297f --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_fontconfig.h @@ -0,0 +1,22 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFontMgr_fontconfig_DEFINED +#define SkFontMgr_fontconfig_DEFINED + +#include "include/core/SkRefCnt.h" +#include + +class SkFontMgr; + +/** Create a font manager around a FontConfig instance. + * If 'fc' is NULL, will use a new default config. + * Takes ownership of 'fc' and will call FcConfigDestroy on it. + */ +SK_API sk_sp SkFontMgr_New_FontConfig(FcConfig* fc); + +#endif // #ifndef SkFontMgr_fontconfig_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_fuchsia.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_fuchsia.h new file mode 100644 index 00000000000..d20530af723 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_fuchsia.h @@ -0,0 +1,19 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFontMgr_fuchsia_DEFINED +#define SkFontMgr_fuchsia_DEFINED + +#include + +#include "include/core/SkRefCnt.h" + +class SkFontMgr; + +SK_API sk_sp SkFontMgr_New_Fuchsia(fuchsia::fonts::ProviderSyncPtr provider); + +#endif // SkFontMgr_fuchsia_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_mac_ct.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_mac_ct.h new file mode 100644 index 00000000000..45cba65b5da --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkFontMgr_mac_ct.h @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFontMgr_mac_ct_DEFINED +#define SkFontMgr_mac_ct_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" + +#ifdef SK_BUILD_FOR_MAC +#import +#endif + +#ifdef SK_BUILD_FOR_IOS +#include +#endif + +class SkFontMgr; + +/** Create a font manager for CoreText. If the collection is nullptr the system default will be used. */ +SK_API extern sk_sp SkFontMgr_New_CoreText(CTFontCollectionRef); + +#endif // SkFontMgr_mac_ct_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkImageGeneratorCG.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkImageGeneratorCG.h new file mode 100644 index 00000000000..6fa775db498 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkImageGeneratorCG.h @@ -0,0 +1,28 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkImageGeneratorCG_DEFINED +#define SkImageGeneratorCG_DEFINED + +// This is needed as clients may override the target platform +// using SkUserConfig +#include "include/private/base/SkLoadUserConfig.h" + +#if defined(SK_BUILD_FOR_MAC) || defined(SK_BUILD_FOR_IOS) + +#include "include/core/SkData.h" +#include "include/core/SkImageGenerator.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +#include + +namespace SkImageGeneratorCG { +SK_API std::unique_ptr MakeFromEncodedCG(sk_sp); +} // namespace SkImageGeneratorCG + +#endif // defined(SK_BUILD_FOR_MAC) || defined(SK_BUILD_FOR_IOS) +#endif // SkImageGeneratorCG_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkImageGeneratorNDK.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkImageGeneratorNDK.h new file mode 100644 index 00000000000..739a586f0d1 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkImageGeneratorNDK.h @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkImageGeneratorNDK_DEFINED +#define SkImageGeneratorNDK_DEFINED + +#include "include/core/SkTypes.h" +#ifdef SK_ENABLE_NDK_IMAGES + +#include "include/core/SkData.h" +#include "include/core/SkImageGenerator.h" + +#include + +namespace SkImageGeneratorNDK { +/** + * Create a generator that uses the Android NDK's APIs for decoding images. + * + * Only supported on devices where __ANDROID_API__ >= 30. + * + * As with SkCodec, the SkColorSpace passed to getPixels() determines the + * type of color space transformations to apply. A null SkColorSpace means to + * apply none. + * + * A note on scaling: Calling getPixels() on the resulting SkImageGenerator + * with dimensions that do not match getInfo() requests a scale. For WebP + * files, dimensions smaller than those of getInfo are supported. For Jpeg + * files, dimensions of 1/2, 1/4, and 1/8 are supported. TODO: Provide an + * API like SkCodecImageGenerator::getScaledDimensions() to report which + * dimensions are supported? + */ +SK_API std::unique_ptr MakeFromEncodedNDK(sk_sp); +} + +#endif // SK_ENABLE_NDK_IMAGES +#endif // SkImageGeneratorNDK_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkImageGeneratorWIC.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkImageGeneratorWIC.h new file mode 100644 index 00000000000..5ea3b83e240 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkImageGeneratorWIC.h @@ -0,0 +1,41 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkImageGeneratorWIC_DEFINED +#define SkImageGeneratorWIC_DEFINED + +#include "include/private/base/SkFeatures.h" + +#if defined(SK_BUILD_FOR_WIN) + +#include "include/core/SkData.h" +#include "include/core/SkImageGenerator.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +#include + +/* + * Any Windows program that uses COM must initialize the COM library by calling + * the CoInitializeEx function. In addition, each thread that uses a COM + * interface must make a separate call to this function. + * + * For every successful call to CoInitializeEx, the thread must call + * CoUninitialize before it exits. + * + * SkImageGeneratorWIC requires the COM library and leaves it to the client to + * initialize COM for their application. + * + * For more information on initializing COM, please see: + * https://msdn.microsoft.com/en-us/library/windows/desktop/ff485844.aspx + */ +namespace SkImageGeneratorWIC { +SK_API std::unique_ptr MakeFromEncodedWIC(sk_sp); +} + +#endif // SK_BUILD_FOR_WIN +#endif // SkImageGeneratorWIC_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkTypeface_fontations.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkTypeface_fontations.h new file mode 100644 index 00000000000..cd6531ab64d --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkTypeface_fontations.h @@ -0,0 +1,21 @@ +/* + * Copyright 2023 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTypeface_fontations_DEFINED +#define SkTypeface_fontations_DEFINED + +#include "include/core/SkFontArguments.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypeface.h" +#include "include/core/SkTypes.h" + +#include + +SK_API sk_sp SkTypeface_Make_Fontations(std::unique_ptr fontData, + const SkFontArguments& args); + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkTypeface_mac.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkTypeface_mac.h new file mode 100644 index 00000000000..ec68e054925 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkTypeface_mac.h @@ -0,0 +1,44 @@ +/* + * Copyright 2011 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTypeface_mac_DEFINED +#define SkTypeface_mac_DEFINED + +#include "include/core/SkTypeface.h" + +#if defined(SK_BUILD_FOR_MAC) || defined(SK_BUILD_FOR_IOS) + +#include + +#ifdef SK_BUILD_FOR_MAC +#import +#endif + +#ifdef SK_BUILD_FOR_IOS +#include +#endif + +/** + * Like the other Typeface make methods, this returns a new reference to the + * corresponding typeface for the specified CTFontRef. + */ +SK_API extern sk_sp SkMakeTypefaceFromCTFont(CTFontRef); + +/** + * Returns the platform-specific CTFontRef handle for a + * given SkTypeface. Note that the returned CTFontRef gets + * released when the source SkTypeface is destroyed. + * + * This method is deprecated. It may only be used by Blink Mac + * legacy code in special cases related to text-shaping + * with AAT fonts, clipboard handling and font fallback. + * See https://code.google.com/p/skia/issues/detail?id=3408 + */ +SK_API extern CTFontRef SkTypeface_GetCTFontRef(const SkTypeface* face); + +#endif // defined(SK_BUILD_FOR_MAC) || defined(SK_BUILD_FOR_IOS) +#endif // SkTypeface_mac_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkTypeface_win.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkTypeface_win.h new file mode 100644 index 00000000000..45711ba2317 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/ports/SkTypeface_win.h @@ -0,0 +1,63 @@ +/* + * Copyright 2011 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTypeface_win_DEFINED +#define SkTypeface_win_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypeface.h" +#include "include/core/SkTypes.h" + +#ifdef SK_BUILD_FOR_WIN + +#ifdef UNICODE +typedef struct tagLOGFONTW LOGFONTW; +typedef LOGFONTW LOGFONT; +#else +typedef struct tagLOGFONTA LOGFONTA; +typedef LOGFONTA LOGFONT; +#endif // UNICODE + +/** + * Like the other Typeface create methods, this returns a new reference to the + * corresponding typeface for the specified logfont. The caller is responsible + * for calling unref() when it is finished. + */ +SK_API sk_sp SkCreateTypefaceFromLOGFONT(const LOGFONT&); + +/** + * Copy the LOGFONT associated with this typeface into the lf parameter. Note + * that the lfHeight will need to be set afterwards, since the typeface does + * not track this (the paint does). + * typeface may be NULL, in which case we return the logfont for the default font. + */ +SK_API void SkLOGFONTFromTypeface(const SkTypeface* typeface, LOGFONT* lf); + +/** + * Set an optional callback to ensure that the data behind a LOGFONT is loaded. + * This will get called if Skia tries to access the data but hits a failure. + * Normally this is null, and is only required if the font data needs to be + * remotely (re)loaded. + */ +SK_API void SkTypeface_SetEnsureLOGFONTAccessibleProc(void (*)(const LOGFONT&)); + +// Experimental! +// +class SkFontMgr; +struct IDWriteFactory; +struct IDWriteFontCollection; +struct IDWriteFontFallback; + +SK_API sk_sp SkFontMgr_New_GDI(); +SK_API sk_sp SkFontMgr_New_DirectWrite(IDWriteFactory* factory = nullptr, + IDWriteFontCollection* collection = nullptr); +SK_API sk_sp SkFontMgr_New_DirectWrite(IDWriteFactory* factory, + IDWriteFontCollection* collection, + IDWriteFontFallback* fallback); + +#endif // SK_BUILD_FOR_WIN +#endif // SkTypeface_win_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/OWNERS b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/OWNERS new file mode 100644 index 00000000000..7cf12a2a7fc --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/OWNERS @@ -0,0 +1,4 @@ +# include/ has a restricted set of reviewers (to limit changes to public API) +# Files in this directory follow the same rules as the rest of Skia, though: + +file:../../OWNERS diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkColorData.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkColorData.h new file mode 100644 index 00000000000..0f6a3e9aa51 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkColorData.h @@ -0,0 +1,385 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkColorData_DEFINED +#define SkColorData_DEFINED + +#include "include/core/SkColor.h" +#include "include/core/SkColorPriv.h" +#include "include/private/base/SkTo.h" + +//////////////////////////////////////////////////////////////////////////////////////////// +// Convert a 16bit pixel to a 32bit pixel + +#define SK_R16_BITS 5 +#define SK_G16_BITS 6 +#define SK_B16_BITS 5 + +#define SK_R16_SHIFT (SK_B16_BITS + SK_G16_BITS) +#define SK_G16_SHIFT (SK_B16_BITS) +#define SK_B16_SHIFT 0 + +#define SK_R16_MASK ((1 << SK_R16_BITS) - 1) +#define SK_G16_MASK ((1 << SK_G16_BITS) - 1) +#define SK_B16_MASK ((1 << SK_B16_BITS) - 1) + +#define SkGetPackedR16(color) (((unsigned)(color) >> SK_R16_SHIFT) & SK_R16_MASK) +#define SkGetPackedG16(color) (((unsigned)(color) >> SK_G16_SHIFT) & SK_G16_MASK) +#define SkGetPackedB16(color) (((unsigned)(color) >> SK_B16_SHIFT) & SK_B16_MASK) + +static inline unsigned SkR16ToR32(unsigned r) { + return (r << (8 - SK_R16_BITS)) | (r >> (2 * SK_R16_BITS - 8)); +} + +static inline unsigned SkG16ToG32(unsigned g) { + return (g << (8 - SK_G16_BITS)) | (g >> (2 * SK_G16_BITS - 8)); +} + +static inline unsigned SkB16ToB32(unsigned b) { + return (b << (8 - SK_B16_BITS)) | (b >> (2 * SK_B16_BITS - 8)); +} + +#define SkPacked16ToR32(c) SkR16ToR32(SkGetPackedR16(c)) +#define SkPacked16ToG32(c) SkG16ToG32(SkGetPackedG16(c)) +#define SkPacked16ToB32(c) SkB16ToB32(SkGetPackedB16(c)) + +////////////////////////////////////////////////////////////////////////////// + +#define SkASSERT_IS_BYTE(x) SkASSERT(0 == ((x) & ~0xFFu)) + +// Reverse the bytes coorsponding to RED and BLUE in a packed pixels. Note the +// pair of them are in the same 2 slots in both RGBA and BGRA, thus there is +// no need to pass in the colortype to this function. +static inline uint32_t SkSwizzle_RB(uint32_t c) { + static const uint32_t kRBMask = (0xFF << SK_R32_SHIFT) | (0xFF << SK_B32_SHIFT); + + unsigned c0 = (c >> SK_R32_SHIFT) & 0xFF; + unsigned c1 = (c >> SK_B32_SHIFT) & 0xFF; + return (c & ~kRBMask) | (c0 << SK_B32_SHIFT) | (c1 << SK_R32_SHIFT); +} + +static inline uint32_t SkPackARGB_as_RGBA(U8CPU a, U8CPU r, U8CPU g, U8CPU b) { + SkASSERT_IS_BYTE(a); + SkASSERT_IS_BYTE(r); + SkASSERT_IS_BYTE(g); + SkASSERT_IS_BYTE(b); + return (a << SK_RGBA_A32_SHIFT) | (r << SK_RGBA_R32_SHIFT) | + (g << SK_RGBA_G32_SHIFT) | (b << SK_RGBA_B32_SHIFT); +} + +static inline uint32_t SkPackARGB_as_BGRA(U8CPU a, U8CPU r, U8CPU g, U8CPU b) { + SkASSERT_IS_BYTE(a); + SkASSERT_IS_BYTE(r); + SkASSERT_IS_BYTE(g); + SkASSERT_IS_BYTE(b); + return (a << SK_BGRA_A32_SHIFT) | (r << SK_BGRA_R32_SHIFT) | + (g << SK_BGRA_G32_SHIFT) | (b << SK_BGRA_B32_SHIFT); +} + +static inline SkPMColor SkSwizzle_RGBA_to_PMColor(uint32_t c) { +#ifdef SK_PMCOLOR_IS_RGBA + return c; +#else + return SkSwizzle_RB(c); +#endif +} + +static inline SkPMColor SkSwizzle_BGRA_to_PMColor(uint32_t c) { +#ifdef SK_PMCOLOR_IS_BGRA + return c; +#else + return SkSwizzle_RB(c); +#endif +} + +////////////////////////////////////////////////////////////////////////////// + +///@{ +/** See ITU-R Recommendation BT.709 at http://www.itu.int/rec/R-REC-BT.709/ .*/ +#define SK_ITU_BT709_LUM_COEFF_R (0.2126f) +#define SK_ITU_BT709_LUM_COEFF_G (0.7152f) +#define SK_ITU_BT709_LUM_COEFF_B (0.0722f) +///@} + +///@{ +/** A float value which specifies this channel's contribution to luminance. */ +#define SK_LUM_COEFF_R SK_ITU_BT709_LUM_COEFF_R +#define SK_LUM_COEFF_G SK_ITU_BT709_LUM_COEFF_G +#define SK_LUM_COEFF_B SK_ITU_BT709_LUM_COEFF_B +///@} + +/** Computes the luminance from the given r, g, and b in accordance with + SK_LUM_COEFF_X. For correct results, r, g, and b should be in linear space. +*/ +static inline U8CPU SkComputeLuminance(U8CPU r, U8CPU g, U8CPU b) { + //The following is + //r * SK_LUM_COEFF_R + g * SK_LUM_COEFF_G + b * SK_LUM_COEFF_B + //with SK_LUM_COEFF_X in 1.8 fixed point (rounding adjusted to sum to 256). + return (r * 54 + g * 183 + b * 19) >> 8; +} + +/** Calculates 256 - (value * alpha256) / 255 in range [0,256], + * for [0,255] value and [0,256] alpha256. + */ +static inline U16CPU SkAlphaMulInv256(U16CPU value, U16CPU alpha256) { + unsigned prod = 0xFFFF - value * alpha256; + return (prod + (prod >> 8)) >> 8; +} + +// The caller may want negative values, so keep all params signed (int) +// so we don't accidentally slip into unsigned math and lose the sign +// extension when we shift (in SkAlphaMul) +static inline int SkAlphaBlend(int src, int dst, int scale256) { + SkASSERT((unsigned)scale256 <= 256); + return dst + SkAlphaMul(src - dst, scale256); +} + +static inline uint16_t SkPackRGB16(unsigned r, unsigned g, unsigned b) { + SkASSERT(r <= SK_R16_MASK); + SkASSERT(g <= SK_G16_MASK); + SkASSERT(b <= SK_B16_MASK); + + return SkToU16((r << SK_R16_SHIFT) | (g << SK_G16_SHIFT) | (b << SK_B16_SHIFT)); +} + +#define SK_R16_MASK_IN_PLACE (SK_R16_MASK << SK_R16_SHIFT) +#define SK_G16_MASK_IN_PLACE (SK_G16_MASK << SK_G16_SHIFT) +#define SK_B16_MASK_IN_PLACE (SK_B16_MASK << SK_B16_SHIFT) + +/////////////////////////////////////////////////////////////////////////////// + +/** + * Abstract 4-byte interpolation, implemented on top of SkPMColor + * utility functions. Third parameter controls blending of the first two: + * (src, dst, 0) returns dst + * (src, dst, 0xFF) returns src + * scale is [0..256], unlike SkFourByteInterp which takes [0..255] + */ +static inline SkPMColor SkFourByteInterp256(SkPMColor src, SkPMColor dst, int scale) { + unsigned a = SkTo(SkAlphaBlend(SkGetPackedA32(src), SkGetPackedA32(dst), scale)); + unsigned r = SkTo(SkAlphaBlend(SkGetPackedR32(src), SkGetPackedR32(dst), scale)); + unsigned g = SkTo(SkAlphaBlend(SkGetPackedG32(src), SkGetPackedG32(dst), scale)); + unsigned b = SkTo(SkAlphaBlend(SkGetPackedB32(src), SkGetPackedB32(dst), scale)); + + return SkPackARGB32(a, r, g, b); +} + +/** + * Abstract 4-byte interpolation, implemented on top of SkPMColor + * utility functions. Third parameter controls blending of the first two: + * (src, dst, 0) returns dst + * (src, dst, 0xFF) returns src + */ +static inline SkPMColor SkFourByteInterp(SkPMColor src, SkPMColor dst, U8CPU srcWeight) { + int scale = (int)SkAlpha255To256(srcWeight); + return SkFourByteInterp256(src, dst, scale); +} + +/** + * 0xAARRGGBB -> 0x00AA00GG, 0x00RR00BB + */ +static inline void SkSplay(uint32_t color, uint32_t* ag, uint32_t* rb) { + const uint32_t mask = 0x00FF00FF; + *ag = (color >> 8) & mask; + *rb = color & mask; +} + +/** + * 0xAARRGGBB -> 0x00AA00GG00RR00BB + * (note, ARGB -> AGRB) + */ +static inline uint64_t SkSplay(uint32_t color) { + const uint32_t mask = 0x00FF00FF; + uint64_t agrb = (color >> 8) & mask; // 0x0000000000AA00GG + agrb <<= 32; // 0x00AA00GG00000000 + agrb |= color & mask; // 0x00AA00GG00RR00BB + return agrb; +} + +/** + * 0xAAxxGGxx, 0xRRxxBBxx-> 0xAARRGGBB + */ +static inline uint32_t SkUnsplay(uint32_t ag, uint32_t rb) { + const uint32_t mask = 0xFF00FF00; + return (ag & mask) | ((rb & mask) >> 8); +} + +/** + * 0xAAxxGGxxRRxxBBxx -> 0xAARRGGBB + * (note, AGRB -> ARGB) + */ +static inline uint32_t SkUnsplay(uint64_t agrb) { + const uint32_t mask = 0xFF00FF00; + return SkPMColor( + ((agrb & mask) >> 8) | // 0x00RR00BB + ((agrb >> 32) & mask)); // 0xAARRGGBB +} + +static inline SkPMColor SkFastFourByteInterp256_32(SkPMColor src, SkPMColor dst, unsigned scale) { + SkASSERT(scale <= 256); + + // Two 8-bit blends per two 32-bit registers, with space to make sure the math doesn't collide. + uint32_t src_ag, src_rb, dst_ag, dst_rb; + SkSplay(src, &src_ag, &src_rb); + SkSplay(dst, &dst_ag, &dst_rb); + + const uint32_t ret_ag = src_ag * scale + (256 - scale) * dst_ag; + const uint32_t ret_rb = src_rb * scale + (256 - scale) * dst_rb; + + return SkUnsplay(ret_ag, ret_rb); +} + +static inline SkPMColor SkFastFourByteInterp256_64(SkPMColor src, SkPMColor dst, unsigned scale) { + SkASSERT(scale <= 256); + // Four 8-bit blends in one 64-bit register, with space to make sure the math doesn't collide. + return SkUnsplay(SkSplay(src) * scale + (256-scale) * SkSplay(dst)); +} + +// TODO(mtklein): Replace slow versions with fast versions, using scale + (scale>>7) everywhere. + +/** + * Same as SkFourByteInterp256, but faster. + */ +static inline SkPMColor SkFastFourByteInterp256(SkPMColor src, SkPMColor dst, unsigned scale) { + // On a 64-bit machine, _64 is about 10% faster than _32, but ~40% slower on a 32-bit machine. + if (sizeof(void*) == 4) { + return SkFastFourByteInterp256_32(src, dst, scale); + } else { + return SkFastFourByteInterp256_64(src, dst, scale); + } +} + +/** + * Nearly the same as SkFourByteInterp, but faster and a touch more accurate, due to better + * srcWeight scaling to [0, 256]. + */ +static inline SkPMColor SkFastFourByteInterp(SkPMColor src, SkPMColor dst, U8CPU srcWeight) { + SkASSERT(srcWeight <= 255); + // scale = srcWeight + (srcWeight >> 7) is more accurate than + // scale = srcWeight + 1, but 7% slower + return SkFastFourByteInterp256(src, dst, srcWeight + (srcWeight >> 7)); +} + +/** + * Interpolates between colors src and dst using [0,256] scale. + */ +static inline SkPMColor SkPMLerp(SkPMColor src, SkPMColor dst, unsigned scale) { + return SkFastFourByteInterp256(src, dst, scale); +} + +static inline SkPMColor SkBlendARGB32(SkPMColor src, SkPMColor dst, U8CPU aa) { + SkASSERT((unsigned)aa <= 255); + + unsigned src_scale = SkAlpha255To256(aa); + unsigned dst_scale = SkAlphaMulInv256(SkGetPackedA32(src), src_scale); + + const uint32_t mask = 0xFF00FF; + + uint32_t src_rb = (src & mask) * src_scale; + uint32_t src_ag = ((src >> 8) & mask) * src_scale; + + uint32_t dst_rb = (dst & mask) * dst_scale; + uint32_t dst_ag = ((dst >> 8) & mask) * dst_scale; + + return (((src_rb + dst_rb) >> 8) & mask) | ((src_ag + dst_ag) & ~mask); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +// Convert a 32bit pixel to a 16bit pixel (no dither) + +#define SkR32ToR16_MACRO(r) ((unsigned)(r) >> (SK_R32_BITS - SK_R16_BITS)) +#define SkG32ToG16_MACRO(g) ((unsigned)(g) >> (SK_G32_BITS - SK_G16_BITS)) +#define SkB32ToB16_MACRO(b) ((unsigned)(b) >> (SK_B32_BITS - SK_B16_BITS)) + +#ifdef SK_DEBUG + static inline unsigned SkR32ToR16(unsigned r) { + SkR32Assert(r); + return SkR32ToR16_MACRO(r); + } + static inline unsigned SkG32ToG16(unsigned g) { + SkG32Assert(g); + return SkG32ToG16_MACRO(g); + } + static inline unsigned SkB32ToB16(unsigned b) { + SkB32Assert(b); + return SkB32ToB16_MACRO(b); + } +#else + #define SkR32ToR16(r) SkR32ToR16_MACRO(r) + #define SkG32ToG16(g) SkG32ToG16_MACRO(g) + #define SkB32ToB16(b) SkB32ToB16_MACRO(b) +#endif + +static inline U16CPU SkPixel32ToPixel16(SkPMColor c) { + unsigned r = ((c >> (SK_R32_SHIFT + (8 - SK_R16_BITS))) & SK_R16_MASK) << SK_R16_SHIFT; + unsigned g = ((c >> (SK_G32_SHIFT + (8 - SK_G16_BITS))) & SK_G16_MASK) << SK_G16_SHIFT; + unsigned b = ((c >> (SK_B32_SHIFT + (8 - SK_B16_BITS))) & SK_B16_MASK) << SK_B16_SHIFT; + return r | g | b; +} + +static inline U16CPU SkPack888ToRGB16(U8CPU r, U8CPU g, U8CPU b) { + return (SkR32ToR16(r) << SK_R16_SHIFT) | + (SkG32ToG16(g) << SK_G16_SHIFT) | + (SkB32ToB16(b) << SK_B16_SHIFT); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static inline SkColor SkPixel16ToColor(U16CPU src) { + SkASSERT(src == SkToU16(src)); + + unsigned r = SkPacked16ToR32(src); + unsigned g = SkPacked16ToG32(src); + unsigned b = SkPacked16ToB32(src); + + SkASSERT((r >> (8 - SK_R16_BITS)) == SkGetPackedR16(src)); + SkASSERT((g >> (8 - SK_G16_BITS)) == SkGetPackedG16(src)); + SkASSERT((b >> (8 - SK_B16_BITS)) == SkGetPackedB16(src)); + + return SkColorSetRGB(r, g, b); +} + +/////////////////////////////////////////////////////////////////////////////// + +typedef uint16_t SkPMColor16; + +// Put in OpenGL order (r g b a) +#define SK_A4444_SHIFT 0 +#define SK_R4444_SHIFT 12 +#define SK_G4444_SHIFT 8 +#define SK_B4444_SHIFT 4 + +static inline U8CPU SkReplicateNibble(unsigned nib) { + SkASSERT(nib <= 0xF); + return (nib << 4) | nib; +} + +#define SkGetPackedA4444(c) (((unsigned)(c) >> SK_A4444_SHIFT) & 0xF) +#define SkGetPackedR4444(c) (((unsigned)(c) >> SK_R4444_SHIFT) & 0xF) +#define SkGetPackedG4444(c) (((unsigned)(c) >> SK_G4444_SHIFT) & 0xF) +#define SkGetPackedB4444(c) (((unsigned)(c) >> SK_B4444_SHIFT) & 0xF) + +#define SkPacked4444ToA32(c) SkReplicateNibble(SkGetPackedA4444(c)) + +static inline SkPMColor SkPixel4444ToPixel32(U16CPU c) { + uint32_t d = (SkGetPackedA4444(c) << SK_A32_SHIFT) | + (SkGetPackedR4444(c) << SK_R32_SHIFT) | + (SkGetPackedG4444(c) << SK_G32_SHIFT) | + (SkGetPackedB4444(c) << SK_B32_SHIFT); + return d | (d << 4); +} + +using SkPMColor4f = SkRGBA4f; + +constexpr SkPMColor4f SK_PMColor4fTRANSPARENT = { 0, 0, 0, 0 }; +constexpr SkPMColor4f SK_PMColor4fBLACK = { 0, 0, 0, 1 }; +constexpr SkPMColor4f SK_PMColor4fWHITE = { 1, 1, 1, 1 }; +constexpr SkPMColor4f SK_PMColor4fILLEGAL = { SK_FloatNegativeInfinity, + SK_FloatNegativeInfinity, + SK_FloatNegativeInfinity, + SK_FloatNegativeInfinity }; +#endif // SkColorData_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkEncodedInfo.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkEncodedInfo.h new file mode 100644 index 00000000000..f1cbc5050c8 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkEncodedInfo.h @@ -0,0 +1,283 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkEncodedInfo_DEFINED +#define SkEncodedInfo_DEFINED + +#include "include/core/SkAlphaType.h" +#include "include/core/SkColorSpace.h" +#include "include/core/SkColorType.h" +#include "include/core/SkData.h" +#include "include/core/SkImageInfo.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkTo.h" +#include "modules/skcms/skcms.h" + +#include +#include +#include + +struct SkEncodedInfo { +public: + class ICCProfile { + public: + static std::unique_ptr Make(sk_sp); + static std::unique_ptr Make(const skcms_ICCProfile&); + + const skcms_ICCProfile* profile() const { return &fProfile; } + sk_sp data() const { return fData; } + private: + ICCProfile(const skcms_ICCProfile&, sk_sp = nullptr); + + skcms_ICCProfile fProfile; + sk_sp fData; + }; + + enum Alpha { + kOpaque_Alpha, + kUnpremul_Alpha, + + // Each pixel is either fully opaque or fully transparent. + // There is no difference between requesting kPremul or kUnpremul. + kBinary_Alpha, + }; + + /* + * We strive to make the number of components per pixel obvious through + * our naming conventions. + * Ex: kRGB has 3 components. kRGBA has 4 components. + * + * This sometimes results in redundant Alpha and Color information. + * Ex: kRGB images must also be kOpaque. + */ + enum Color { + // PNG, WBMP + kGray_Color, + + // PNG + kGrayAlpha_Color, + + // PNG with Skia-specific sBIT + // Like kGrayAlpha, except this expects to be treated as + // kAlpha_8_SkColorType, which ignores the gray component. If + // decoded to full color (e.g. kN32), the gray component is respected + // (so it can share code with kGrayAlpha). + kXAlpha_Color, + + // PNG + // 565 images may be encoded to PNG by specifying the number of + // significant bits for each channel. This is a strange 565 + // representation because the image is still encoded with 8 bits per + // component. + k565_Color, + + // PNG, GIF, BMP + kPalette_Color, + + // PNG, RAW + kRGB_Color, + kRGBA_Color, + + // BMP + kBGR_Color, + kBGRX_Color, + kBGRA_Color, + + // JPEG, WEBP + kYUV_Color, + + // WEBP + kYUVA_Color, + + // JPEG + // Photoshop actually writes inverted CMYK data into JPEGs, where zero + // represents 100% ink coverage. For this reason, we treat CMYK JPEGs + // as having inverted CMYK. libjpeg-turbo warns that this may break + // other applications, but the CMYK JPEGs we see on the web expect to + // be treated as inverted CMYK. + kInvertedCMYK_Color, + kYCCK_Color, + }; + + static SkEncodedInfo Make(int width, int height, Color color, Alpha alpha, + int bitsPerComponent) { + return Make(width, height, color, alpha, bitsPerComponent, nullptr); + } + + static SkEncodedInfo Make(int width, int height, Color color, + Alpha alpha, int bitsPerComponent, std::unique_ptr profile) { + return Make(width, height, color, alpha, /*bitsPerComponent*/ bitsPerComponent, + std::move(profile), /*colorDepth*/ bitsPerComponent); + } + + static SkEncodedInfo Make(int width, int height, Color color, + Alpha alpha, int bitsPerComponent, std::unique_ptr profile, + int colorDepth) { + SkASSERT(1 == bitsPerComponent || + 2 == bitsPerComponent || + 4 == bitsPerComponent || + 8 == bitsPerComponent || + 16 == bitsPerComponent); + + switch (color) { + case kGray_Color: + SkASSERT(kOpaque_Alpha == alpha); + break; + case kGrayAlpha_Color: + SkASSERT(kOpaque_Alpha != alpha); + break; + case kPalette_Color: + SkASSERT(16 != bitsPerComponent); + break; + case kRGB_Color: + case kBGR_Color: + case kBGRX_Color: + SkASSERT(kOpaque_Alpha == alpha); + SkASSERT(bitsPerComponent >= 8); + break; + case kYUV_Color: + case kInvertedCMYK_Color: + case kYCCK_Color: + SkASSERT(kOpaque_Alpha == alpha); + SkASSERT(8 == bitsPerComponent); + break; + case kRGBA_Color: + SkASSERT(bitsPerComponent >= 8); + break; + case kBGRA_Color: + case kYUVA_Color: + SkASSERT(8 == bitsPerComponent); + break; + case kXAlpha_Color: + SkASSERT(kUnpremul_Alpha == alpha); + SkASSERT(8 == bitsPerComponent); + break; + case k565_Color: + SkASSERT(kOpaque_Alpha == alpha); + SkASSERT(8 == bitsPerComponent); + break; + default: + SkASSERT(false); + break; + } + + return SkEncodedInfo(width, + height, + color, + alpha, + SkToU8(bitsPerComponent), + SkToU8(colorDepth), + std::move(profile)); + } + + /* + * Returns a recommended SkImageInfo. + * + * TODO: Leave this up to the client. + */ + SkImageInfo makeImageInfo() const { + auto ct = kGray_Color == fColor ? kGray_8_SkColorType : + kXAlpha_Color == fColor ? kAlpha_8_SkColorType : + k565_Color == fColor ? kRGB_565_SkColorType : + kN32_SkColorType ; + auto alpha = kOpaque_Alpha == fAlpha ? kOpaque_SkAlphaType + : kUnpremul_SkAlphaType; + sk_sp cs = fProfile ? SkColorSpace::Make(*fProfile->profile()) + : nullptr; + if (!cs) { + cs = SkColorSpace::MakeSRGB(); + } + return SkImageInfo::Make(fWidth, fHeight, ct, alpha, std::move(cs)); + } + + int width() const { return fWidth; } + int height() const { return fHeight; } + Color color() const { return fColor; } + Alpha alpha() const { return fAlpha; } + bool opaque() const { return fAlpha == kOpaque_Alpha; } + const skcms_ICCProfile* profile() const { + if (!fProfile) return nullptr; + return fProfile->profile(); + } + sk_sp profileData() const { + if (!fProfile) return nullptr; + return fProfile->data(); + } + + uint8_t bitsPerComponent() const { return fBitsPerComponent; } + + uint8_t bitsPerPixel() const { + switch (fColor) { + case kGray_Color: + return fBitsPerComponent; + case kXAlpha_Color: + case kGrayAlpha_Color: + return 2 * fBitsPerComponent; + case kPalette_Color: + return fBitsPerComponent; + case kRGB_Color: + case kBGR_Color: + case kYUV_Color: + case k565_Color: + return 3 * fBitsPerComponent; + case kRGBA_Color: + case kBGRA_Color: + case kBGRX_Color: + case kYUVA_Color: + case kInvertedCMYK_Color: + case kYCCK_Color: + return 4 * fBitsPerComponent; + default: + SkASSERT(false); + return 0; + } + } + + SkEncodedInfo(const SkEncodedInfo& orig) = delete; + SkEncodedInfo& operator=(const SkEncodedInfo&) = delete; + + SkEncodedInfo(SkEncodedInfo&& orig) = default; + SkEncodedInfo& operator=(SkEncodedInfo&&) = default; + + // Explicit copy method, to avoid accidental copying. + SkEncodedInfo copy() const { + auto copy = SkEncodedInfo::Make( + fWidth, fHeight, fColor, fAlpha, fBitsPerComponent, nullptr, fColorDepth); + if (fProfile) { + copy.fProfile = std::make_unique(*fProfile); + } + return copy; + } + + // Return number of bits of R/G/B channel + uint8_t getColorDepth() const { + return fColorDepth; + } + +private: + SkEncodedInfo(int width, int height, Color color, Alpha alpha, + uint8_t bitsPerComponent, uint8_t colorDepth, std::unique_ptr profile) + : fWidth(width) + , fHeight(height) + , fColor(color) + , fAlpha(alpha) + , fBitsPerComponent(bitsPerComponent) + , fColorDepth(colorDepth) + , fProfile(std::move(profile)) + {} + + int fWidth; + int fHeight; + Color fColor; + Alpha fAlpha; + uint8_t fBitsPerComponent; + uint8_t fColorDepth; + std::unique_ptr fProfile; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkExif.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkExif.h new file mode 100644 index 00000000000..41abad34322 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkExif.h @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkExif_DEFINED +#define SkExif_DEFINED + +#include "include/codec/SkEncodedOrigin.h" +#include "include/private/base/SkAPI.h" + +#include +#include + +class SkData; + +namespace SkExif { + +// Tag values that are parsed by Parse and stored in Metadata. +static constexpr uint16_t kOriginTag = 0x112; +static constexpr uint16_t kResolutionUnitTag = 0x0128; +static constexpr uint16_t kXResolutionTag = 0x011a; +static constexpr uint16_t kYResolutionTag = 0x011b; +static constexpr uint16_t kPixelXDimensionTag = 0xa002; +static constexpr uint16_t kPixelYDimensionTag = 0xa003; + +struct Metadata { + // The image orientation. + std::optional fOrigin; + + // The HDR headroom property. + // https://developer.apple.com/documentation/appkit/images_and_pdf/applying_apple_hdr_effect_to_your_photos + std::optional fHdrHeadroom; + + // Resolution. + std::optional fResolutionUnit; + std::optional fXResolution; + std::optional fYResolution; + + // Size in pixels. + std::optional fPixelXDimension; + std::optional fPixelYDimension; +}; + +/* + * Parse the metadata specified in |data| and write them to |metadata|. Stop only at an + * unrecoverable error (allow truncated input). + */ +void SK_API Parse(Metadata& metadata, const SkData* data); + +} // namespace SkExif + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkGainmapInfo.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkGainmapInfo.h new file mode 100644 index 00000000000..033c9333cb9 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkGainmapInfo.h @@ -0,0 +1,140 @@ +/* + * Copyright 2023 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkGainmapInfo_DEFINED +#define SkGainmapInfo_DEFINED + +#include "include/core/SkColor.h" +#include "include/core/SkColorSpace.h" +#include "include/core/SkRefCnt.h" +class SkData; + +/** + * Gainmap rendering parameters. Suppose our display has HDR to SDR ratio of H and we wish to + * display an image with gainmap on this display. Let B be the pixel value from the base image + * in a color space that has the primaries of the base image and a linear transfer function. Let + * G be the pixel value from the gainmap. Let D be the output pixel in the same color space as B. + * The value of D is computed as follows: + * + * First, let W be a weight parameter determing how much the gainmap will be applied. + * W = clamp((log(H) - log(fDisplayRatioSdr)) / + * (log(fDisplayRatioHdr) - log(fDisplayRatioSdr), 0, 1) + * + * Next, let L be the gainmap value in log space. We compute this from the value G that was + * sampled from the texture as follows: + * L = mix(log(fGainmapRatioMin), log(fGainmapRatioMax), pow(G, fGainmapGamma)) + * + * Finally, apply the gainmap to compute D, the displayed pixel. If the base image is SDR then + * compute: + * D = (B + fEpsilonSdr) * exp(L * W) - fEpsilonHdr + * If the base image is HDR then compute: + * D = (B + fEpsilonHdr) * exp(L * (W - 1)) - fEpsilonSdr + * + * In the above math, log() is a natural logarithm and exp() is natural exponentiation. Note, + * however, that the base used for the log() and exp() functions does not affect the results of + * the computation (it cancels out, as long as the same base is used throughout). + * + * This product includes Gain Map technology under license by Adobe. + */ +struct SkGainmapInfo { + /** + * Parameters for converting the gainmap from its image encoding to log space. These are + * specified per color channel. The alpha value is unused. + */ + SkColor4f fGainmapRatioMin = {1.f, 1.f, 1.f, 1.0}; + SkColor4f fGainmapRatioMax = {2.f, 2.f, 2.f, 1.0}; + SkColor4f fGainmapGamma = {1.f, 1.f, 1.f, 1.f}; + + /** + * Parameters sometimes used in gainmap computation to avoid numerical instability. + */ + SkColor4f fEpsilonSdr = {0.f, 0.f, 0.f, 1.0}; + SkColor4f fEpsilonHdr = {0.f, 0.f, 0.f, 1.0}; + + /** + * If the output display's HDR to SDR ratio is less or equal than fDisplayRatioSdr then the SDR + * rendition is displayed. If the output display's HDR to SDR ratio is greater or equal than + * fDisplayRatioHdr then the HDR rendition is displayed. If the output display's HDR to SDR + * ratio is between these values then an interpolation between the two is displayed using the + * math above. + */ + float fDisplayRatioSdr = 1.f; + float fDisplayRatioHdr = 2.f; + + /** + * Whether the base image is the SDR image or the HDR image. + */ + enum class BaseImageType { + kSDR, + kHDR, + }; + BaseImageType fBaseImageType = BaseImageType::kSDR; + + /** + * The type of the gainmap image. If the type is kApple, then the gainmap image was originally + * encoded according to the specification at [0], and can be converted to the kDefault type by + * applying the transformation described at [1]. + * [0] https://developer.apple.com/documentation/appkit/images_and_pdf/ + * applying_apple_hdr_effect_to_your_photos + * [1] https://docs.google.com/document/d/1iUpYAThVV_FuDdeiO3t0vnlfoA1ryq0WfGS9FuydwKc + */ + enum class Type { + kDefault, + kApple, + }; + Type fType = Type::kDefault; + + /** + * If specified, color space to apply the gainmap in, otherwise the base image's color space + * is used. Only the color primaries are used, the transfer function is irrelevant. + */ + sk_sp fGainmapMathColorSpace = nullptr; + + /** + * Return true if this can be encoded as an UltraHDR v1 image. + */ + bool isUltraHDRv1Compatible() const; + + /** + * If |data| contains an ISO 21496-1 version that is supported, return true. Otherwise return + * false. + */ + static bool ParseVersion(const SkData* data); + + /** + * If |data| constains ISO 21496-1 metadata then parse that metadata then use it to populate + * |info| and return true, otherwise return false. If |data| indicates that that the base image + * color space primaries should be used for gainmap application then set + * |fGainmapMathColorSpace| to nullptr, otherwise set |fGainmapMathColorSpace| to sRGB (the + * default, to be overwritten by the image decoder). + */ + static bool Parse(const SkData* data, SkGainmapInfo& info); + + /** + * Serialize an ISO 21496-1 version 0 blob containing only the version structure. + */ + static sk_sp SerializeVersion(); + + /** + * Serialize an ISO 21496-1 version 0 blob containing this' gainmap parameters. + */ + sk_sp serialize() const; + + inline bool operator==(const SkGainmapInfo& other) const { + return fGainmapRatioMin == other.fGainmapRatioMin && + fGainmapRatioMax == other.fGainmapRatioMax && fGainmapGamma == other.fGainmapGamma && + fEpsilonSdr == other.fEpsilonSdr && fEpsilonHdr == other.fEpsilonHdr && + fDisplayRatioSdr == other.fDisplayRatioSdr && + fDisplayRatioHdr == other.fDisplayRatioHdr && + fBaseImageType == other.fBaseImageType && fType == other.fType && + SkColorSpace::Equals(fGainmapMathColorSpace.get(), + other.fGainmapMathColorSpace.get()); + } + inline bool operator!=(const SkGainmapInfo& other) const { return !(*this == other); } +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkGainmapShader.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkGainmapShader.h new file mode 100644 index 00000000000..bc531124c7e --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkGainmapShader.h @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkGainmapShader_DEFINED +#define SkGainmapShader_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +class SkColorSpace; +class SkShader; +class SkImage; +struct SkGainmapInfo; +struct SkRect; +struct SkSamplingOptions; + +/** + * A gainmap shader will apply a gainmap to an base image using the math described alongside the + * definition of SkGainmapInfo. + */ +class SK_API SkGainmapShader { +public: + /** + * Make a gainmap shader. + * + * When sampling the base image baseImage, the rectangle baseRect will be sampled to map to + * the rectangle dstRect. Sampling will be done according to baseSamplingOptions. + * + * When sampling the gainmap image gainmapImage, the rectangle gainmapRect will be sampled to + * map to the rectangle dstRect. Sampling will be done according to gainmapSamplingOptions. + * + * The gainmap will be applied according to the HDR to SDR ratio specified in dstHdrRatio. + * + * This shader must know the color space of the canvas that it will be rendered to. This color + * space must be specified in dstColorSpace. + * TODO(ccameron): Remove the need for dstColorSpace. + */ + static sk_sp Make(const sk_sp& baseImage, + const SkRect& baseRect, + const SkSamplingOptions& baseSamplingOptions, + const sk_sp& gainmapImage, + const SkRect& gainmapRect, + const SkSamplingOptions& gainmapSamplingOptions, + const SkGainmapInfo& gainmapInfo, + const SkRect& dstRect, + float dstHdrRatio, + sk_sp dstColorSpace); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkIDChangeListener.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkIDChangeListener.h new file mode 100644 index 00000000000..8ebb6ca18e5 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkIDChangeListener.h @@ -0,0 +1,76 @@ +/* + * Copyright 2020 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkIDChangeListener_DEFINED +#define SkIDChangeListener_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkMutex.h" +#include "include/private/base/SkTArray.h" +#include "include/private/base/SkThreadAnnotations.h" + +#include + +/** + * Used to be notified when a gen/unique ID is invalidated, typically to preemptively purge + * associated items from a cache that are no longer reachable. The listener can + * be marked for deregistration if the cached item is remove before the listener is + * triggered. This prevents unbounded listener growth when cache items are routinely + * removed before the gen ID/unique ID is invalidated. + */ +class SkIDChangeListener : public SkRefCnt { +public: + SkIDChangeListener(); + + ~SkIDChangeListener() override; + + virtual void changed() = 0; + + /** + * Mark the listener is no longer needed. It should be removed and changed() should not be + * called. + */ + void markShouldDeregister() { fShouldDeregister.store(true, std::memory_order_relaxed); } + + /** Indicates whether markShouldDeregister was called. */ + bool shouldDeregister() { return fShouldDeregister.load(std::memory_order_acquire); } + + /** Manages a list of SkIDChangeListeners. */ + class List { + public: + List(); + + ~List(); + + /** + * Add a new listener to the list. It must not already be deregistered. Also clears out + * previously deregistered listeners. + */ + void add(sk_sp listener) SK_EXCLUDES(fMutex); + + /** + * The number of registered listeners (including deregisterd listeners that are yet-to-be + * removed. + */ + int count() const SK_EXCLUDES(fMutex); + + /** Calls changed() on all listeners that haven't been deregistered and resets the list. */ + void changed() SK_EXCLUDES(fMutex); + + /** Resets without calling changed() on the listeners. */ + void reset() SK_EXCLUDES(fMutex); + + private: + mutable SkMutex fMutex; + skia_private::STArray<1, sk_sp> fListeners SK_GUARDED_BY(fMutex); + }; + +private: + std::atomic fShouldDeregister; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkJpegGainmapEncoder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkJpegGainmapEncoder.h new file mode 100644 index 00000000000..0b4d4babc8f --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkJpegGainmapEncoder.h @@ -0,0 +1,48 @@ +/* + * Copyright 2023 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkJpegGainmapEncoder_DEFINED +#define SkJpegGainmapEncoder_DEFINED + +#include "include/encode/SkJpegEncoder.h" + +class SkPixmap; +class SkWStream; +struct SkGainmapInfo; + +class SK_API SkJpegGainmapEncoder { +public: + /** + * Encode an UltraHDR image to |dst|. + * + * The base image is specified by |base|, and |baseOptions| controls the encoding behavior for + * the base image. + * + * The gainmap image is specified by |gainmap|, and |gainmapOptions| controls the encoding + * behavior for the gainmap image. + * + * The rendering behavior of the gainmap image is provided in |gainmapInfo|. + * + * If |baseOptions| or |gainmapOptions| specify XMP metadata, then that metadata will be + * overwritten. + * + * Returns true on success. Returns false on an invalid or unsupported |src|. + */ + static bool EncodeHDRGM(SkWStream* dst, + const SkPixmap& base, + const SkJpegEncoder::Options& baseOptions, + const SkPixmap& gainmap, + const SkJpegEncoder::Options& gainmapOptions, + const SkGainmapInfo& gainmapInfo); + + /** + * Write a Multi Picture Format containing the |imageCount| images specified by |images|. + */ + static bool MakeMPF(SkWStream* dst, const SkData** images, size_t imageCount); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkJpegMetadataDecoder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkJpegMetadataDecoder.h new file mode 100644 index 00000000000..c9e0b3406dc --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkJpegMetadataDecoder.h @@ -0,0 +1,86 @@ +/* + * Copyright 2013 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkJpegMetadataDecoder_DEFINED +#define SkJpegMetadataDecoder_DEFINED + +#include "include/core/SkData.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" + +#include +#include + +struct SkGainmapInfo; + +/** + * An interface that can be used to extract metadata from an encoded JPEG file. + */ +class SK_API SkJpegMetadataDecoder { +public: + SkJpegMetadataDecoder() {} + virtual ~SkJpegMetadataDecoder() {} + + SkJpegMetadataDecoder(const SkJpegMetadataDecoder&) = delete; + SkJpegMetadataDecoder& operator=(const SkJpegMetadataDecoder&) = delete; + + /** + * A segment from a JPEG file. This is usually populated from a jpeg_marker_struct. + */ + struct SK_API Segment { + Segment(uint8_t marker, sk_sp data) : fMarker(marker), fData(std::move(data)) {} + + // The segment's marker. + uint8_t fMarker = 0; + + // The segment's parameters (not including the marker and parameter length). + sk_sp fData; + }; + + /** + * Create metadata for the specified segments from a JPEG file's header (defined as all segments + * before the first StartOfScan). This may return nullptr. + */ + static std::unique_ptr Make(std::vector headerSegments); + + /** + * Return the Exif data attached to the image (if any) and nullptr otherwise. If |copyData| is + * false, then the returned SkData may directly reference the data provided when this object was + * created. + */ + virtual sk_sp getExifMetadata(bool copyData) const = 0; + + /** + * Return the ICC profile of the image if any, and nullptr otherwise. If |copyData| is false, + * then the returned SkData may directly reference the data provided when this object was + * created. + */ + virtual sk_sp getICCProfileData(bool copyData) const = 0; + + /** + * Return the ISO 21496-1 metadata, if any, and nullptr otherwise. If |copyData| is false, + * then the returned SkData may directly reference the data provided when this object was + * created. + */ + virtual sk_sp getISOGainmapMetadata(bool copyData) const = 0; + + /** + * Return true if there is a possibility that this image contains a gainmap image. + */ + virtual bool mightHaveGainmapImage() const = 0; + + /** + * Given a JPEG encoded image |baseImageData|, return in |outGainmapImageData| the JPEG encoded + * gainmap image and return in |outGainmapInfo| its gainmap rendering parameters. Return true if + * both output variables were successfully populated, otherwise return false. + */ + virtual bool findGainmapImage(sk_sp baseImageData, + sk_sp& outGainmapImagedata, + SkGainmapInfo& outGainmapInfo) = 0; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkPathRef.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkPathRef.h new file mode 100644 index 00000000000..6b2d8eccee9 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkPathRef.h @@ -0,0 +1,582 @@ +/* + * Copyright 2012 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPathRef_DEFINED +#define SkPathRef_DEFINED + +#include "include/core/SkArc.h" +#include "include/core/SkPoint.h" +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" +#include "include/private/SkIDChangeListener.h" +#include "include/private/base/SkDebug.h" +#include "include/private/base/SkTArray.h" +#include "include/private/base/SkTo.h" + +#include +#include +#include +#include +#include + +class SkMatrix; +class SkRRect; + +// These are computed from a stream of verbs +struct SkPathVerbAnalysis { + bool valid; + int points, weights; + unsigned segmentMask; +}; +SkPathVerbAnalysis sk_path_analyze_verbs(const uint8_t verbs[], int count); + + +/** + * Holds the path verbs and points. It is versioned by a generation ID. None of its public methods + * modify the contents. To modify or append to the verbs/points wrap the SkPathRef in an + * SkPathRef::Editor object. Installing the editor resets the generation ID. It also performs + * copy-on-write if the SkPathRef is shared by multiple SkPaths. The caller passes the Editor's + * constructor a pointer to a sk_sp, which may be updated to point to a new SkPathRef + * after the editor's constructor returns. + * + * The points and verbs are stored in a single allocation. The points are at the begining of the + * allocation while the verbs are stored at end of the allocation, in reverse order. Thus the points + * and verbs both grow into the middle of the allocation until the meet. To access verb i in the + * verb array use ref.verbs()[~i] (because verbs() returns a pointer just beyond the first + * logical verb or the last verb in memory). + */ + +class SK_API SkPathRef final : public SkNVRefCnt { +public: + // See https://bugs.chromium.org/p/skia/issues/detail?id=13817 for how these sizes were + // determined. + using PointsArray = skia_private::STArray<4, SkPoint>; + using VerbsArray = skia_private::STArray<4, uint8_t>; + using ConicWeightsArray = skia_private::STArray<2, SkScalar>; + + enum class PathType : uint8_t { + kGeneral, + kOval, + kOpenOval, // An unclosed oval, as is generated by canvas2d ellipse or arc + kRRect, + kArc, + }; + + SkPathRef(PointsArray points, VerbsArray verbs, ConicWeightsArray weights, + unsigned segmentMask) + : fPoints(std::move(points)) + , fVerbs(std::move(verbs)) + , fConicWeights(std::move(weights)) + { + fBoundsIsDirty = true; // this also invalidates fIsFinite + fGenerationID = 0; // recompute + fSegmentMask = segmentMask; + fType = PathType::kGeneral; + // The next two values don't matter unless fType is kOval or kRRect + fRRectOrOvalIsCCW = false; + fRRectOrOvalStartIdx = 0xAC; + fArcOval.setEmpty(); + fArcStartAngle = fArcSweepAngle = 0.0f; + fArcType = SkArc::Type::kArc; + SkDEBUGCODE(fEditorsAttached.store(0);) + + this->computeBounds(); // do this now, before we worry about multiple owners/threads + SkDEBUGCODE(this->validate();) + } + + class Editor { + public: + Editor(sk_sp* pathRef, + int incReserveVerbs = 0, + int incReservePoints = 0, + int incReserveConics = 0); + + ~Editor() { SkDEBUGCODE(fPathRef->fEditorsAttached--;) } + + /** + * Returns the array of points. + */ + SkPoint* writablePoints() { return fPathRef->getWritablePoints(); } + const SkPoint* points() const { return fPathRef->points(); } + + /** + * Gets the ith point. Shortcut for this->points() + i + */ + SkPoint* atPoint(int i) { return fPathRef->getWritablePoints() + i; } + const SkPoint* atPoint(int i) const { return &fPathRef->fPoints[i]; } + + /** + * Adds the verb and allocates space for the number of points indicated by the verb. The + * return value is a pointer to where the points for the verb should be written. + * 'weight' is only used if 'verb' is kConic_Verb + */ + SkPoint* growForVerb(int /*SkPath::Verb*/ verb, SkScalar weight = 0) { + SkDEBUGCODE(fPathRef->validate();) + return fPathRef->growForVerb(verb, weight); + } + + /** + * Allocates space for multiple instances of a particular verb and the + * requisite points & weights. + * The return pointer points at the first new point (indexed normally []). + * If 'verb' is kConic_Verb, 'weights' will return a pointer to the + * space for the conic weights (indexed normally). + */ + SkPoint* growForRepeatedVerb(int /*SkPath::Verb*/ verb, + int numVbs, + SkScalar** weights = nullptr) { + return fPathRef->growForRepeatedVerb(verb, numVbs, weights); + } + + /** + * Concatenates all verbs from 'path' onto the pathRef's verbs array. Increases the point + * count by the number of points in 'path', and the conic weight count by the number of + * conics in 'path'. + * + * Returns pointers to the uninitialized points and conic weights data. + */ + std::tuple growForVerbsInPath(const SkPathRef& path) { + return fPathRef->growForVerbsInPath(path); + } + + /** + * Resets the path ref to a new verb and point count. The new verbs and points are + * uninitialized. + */ + void resetToSize(int newVerbCnt, int newPointCnt, int newConicCount) { + fPathRef->resetToSize(newVerbCnt, newPointCnt, newConicCount); + } + + /** + * Gets the path ref that is wrapped in the Editor. + */ + SkPathRef* pathRef() { return fPathRef; } + + void setIsOval(bool isCCW, unsigned start, bool isClosed) { + fPathRef->setIsOval(isCCW, start, isClosed); + } + + void setIsRRect(bool isCCW, unsigned start) { + fPathRef->setIsRRect(isCCW, start); + } + + void setIsArc(const SkArc& arc) { + fPathRef->setIsArc(arc); + } + + void setBounds(const SkRect& rect) { fPathRef->setBounds(rect); } + + private: + SkPathRef* fPathRef; + }; + + class SK_API Iter { + public: + Iter(); + Iter(const SkPathRef&); + + void setPathRef(const SkPathRef&); + + /** Return the next verb in this iteration of the path. When all + segments have been visited, return kDone_Verb. + + If any point in the path is non-finite, return kDone_Verb immediately. + + @param pts The points representing the current verb and/or segment + This must not be NULL. + @return The verb for the current segment + */ + uint8_t next(SkPoint pts[4]); + uint8_t peek() const; + + SkScalar conicWeight() const { return *fConicWeights; } + + private: + const SkPoint* fPts; + const uint8_t* fVerbs; + const uint8_t* fVerbStop; + const SkScalar* fConicWeights; + }; + +public: + /** + * Gets a path ref with no verbs or points. + */ + static SkPathRef* CreateEmpty(); + + /** + * Returns true if all of the points in this path are finite, meaning there + * are no infinities and no NaNs. + */ + bool isFinite() const { + if (fBoundsIsDirty) { + this->computeBounds(); + } + return SkToBool(fIsFinite); + } + + /** + * Returns a mask, where each bit corresponding to a SegmentMask is + * set if the path contains 1 or more segments of that type. + * Returns 0 for an empty path (no segments). + */ + uint32_t getSegmentMasks() const { return fSegmentMask; } + + /** Returns true if the path is an oval. + * + * @param rect returns the bounding rect of this oval. It's a circle + * if the height and width are the same. + * @param isCCW is the oval CCW (or CW if false). + * @param start indicates where the contour starts on the oval (see + * SkPath::addOval for intepretation of the index). + * + * @return true if this path is an oval. + * Tracking whether a path is an oval is considered an + * optimization for performance and so some paths that are in + * fact ovals can report false. + */ + bool isOval(SkRect* rect, bool* isCCW, unsigned* start) const { + if (fType == PathType::kOval) { + if (rect) { + *rect = this->getBounds(); + } + if (isCCW) { + *isCCW = SkToBool(fRRectOrOvalIsCCW); + } + if (start) { + *start = fRRectOrOvalStartIdx; + } + } + + return fType == PathType::kOval; + } + + bool isRRect(SkRRect* rrect, bool* isCCW, unsigned* start) const; + + bool isArc(SkArc* arc) const { + if (fType == PathType::kArc) { + if (arc) { + *arc = SkArc::Make(fArcOval, fArcStartAngle, fArcSweepAngle, fArcType); + } + } + + return fType == PathType::kArc; + } + + bool hasComputedBounds() const { + return !fBoundsIsDirty; + } + + /** Returns the bounds of the path's points. If the path contains 0 or 1 + points, the bounds is set to (0,0,0,0), and isEmpty() will return true. + Note: this bounds may be larger than the actual shape, since curves + do not extend as far as their control points. + */ + const SkRect& getBounds() const { + if (fBoundsIsDirty) { + this->computeBounds(); + } + return fBounds; + } + + SkRRect getRRect() const; + + /** + * Transforms a path ref by a matrix, allocating a new one only if necessary. + */ + static void CreateTransformedCopy(sk_sp* dst, + const SkPathRef& src, + const SkMatrix& matrix); + + // static SkPathRef* CreateFromBuffer(SkRBuffer* buffer); + + /** + * Rollsback a path ref to zero verbs and points with the assumption that the path ref will be + * repopulated with approximately the same number of verbs and points. A new path ref is created + * only if necessary. + */ + static void Rewind(sk_sp* pathRef); + + ~SkPathRef(); + int countPoints() const { return fPoints.size(); } + int countVerbs() const { return fVerbs.size(); } + int countWeights() const { return fConicWeights.size(); } + + size_t approximateBytesUsed() const; + + /** + * Returns a pointer one beyond the first logical verb (last verb in memory order). + */ + const uint8_t* verbsBegin() const { return fVerbs.begin(); } + + /** + * Returns a const pointer to the first verb in memory (which is the last logical verb). + */ + const uint8_t* verbsEnd() const { return fVerbs.end(); } + + /** + * Returns a const pointer to the first point. + */ + const SkPoint* points() const { return fPoints.begin(); } + + /** + * Shortcut for this->points() + this->countPoints() + */ + const SkPoint* pointsEnd() const { return this->points() + this->countPoints(); } + + const SkScalar* conicWeights() const { return fConicWeights.begin(); } + const SkScalar* conicWeightsEnd() const { return fConicWeights.end(); } + + /** + * Convenience methods for getting to a verb or point by index. + */ + uint8_t atVerb(int index) const { return fVerbs[index]; } + const SkPoint& atPoint(int index) const { return fPoints[index]; } + + bool operator== (const SkPathRef& ref) const; + + void interpolate(const SkPathRef& ending, SkScalar weight, SkPathRef* out) const; + + /** + * Gets an ID that uniquely identifies the contents of the path ref. If two path refs have the + * same ID then they have the same verbs and points. However, two path refs may have the same + * contents but different genIDs. + * skbug.com/1762 for background on why fillType is necessary (for now). + */ + uint32_t genID(uint8_t fillType) const; + + void addGenIDChangeListener(sk_sp); // Threadsafe. + int genIDChangeListenerCount(); // Threadsafe + + bool dataMatchesVerbs() const; + bool isValid() const; + SkDEBUGCODE(void validate() const { SkASSERT(this->isValid()); } ) + + /** + * Resets this SkPathRef to a clean state. + */ + void reset(); + + bool isInitialEmptyPathRef() const { + return fGenerationID == kEmptyGenID; + } + +private: + enum SerializationOffsets { + kLegacyRRectOrOvalStartIdx_SerializationShift = 28, // requires 3 bits, ignored. + kLegacyRRectOrOvalIsCCW_SerializationShift = 27, // requires 1 bit, ignored. + kLegacyIsRRect_SerializationShift = 26, // requires 1 bit, ignored. + kIsFinite_SerializationShift = 25, // requires 1 bit + kLegacyIsOval_SerializationShift = 24, // requires 1 bit, ignored. + kSegmentMask_SerializationShift = 0 // requires 4 bits (deprecated) + }; + + SkPathRef(int numVerbs = 0, int numPoints = 0, int numConics = 0) { + fBoundsIsDirty = true; // this also invalidates fIsFinite + fGenerationID = kEmptyGenID; + fSegmentMask = 0; + fType = PathType::kGeneral; + // The next two values don't matter unless fType is kOval or kRRect + fRRectOrOvalIsCCW = false; + fRRectOrOvalStartIdx = 0xAC; + fArcOval.setEmpty(); + fArcStartAngle = fArcSweepAngle = 0.0f; + fArcType = SkArc::Type::kArc; + if (numPoints > 0) { + fPoints.reserve_exact(numPoints); + } + if (numVerbs > 0) { + fVerbs.reserve_exact(numVerbs); + } + if (numConics > 0) { + fConicWeights.reserve_exact(numConics); + } + SkDEBUGCODE(fEditorsAttached.store(0);) + SkDEBUGCODE(this->validate();) + } + + void copy(const SkPathRef& ref, int additionalReserveVerbs, int additionalReservePoints, int additionalReserveConics); + + // Return true if the computed bounds are finite. + static bool ComputePtBounds(SkRect* bounds, const SkPathRef& ref) { + return bounds->setBoundsCheck(ref.points(), ref.countPoints()); + } + + // called, if dirty, by getBounds() + void computeBounds() const { + SkDEBUGCODE(this->validate();) + // TODO: remove fBoundsIsDirty and fIsFinite, + // using an inverted rect instead of fBoundsIsDirty and always recalculating fIsFinite. + SkASSERT(fBoundsIsDirty); + + fIsFinite = ComputePtBounds(&fBounds, *this); + fBoundsIsDirty = false; + } + + void setBounds(const SkRect& rect) { + SkASSERT(rect.fLeft <= rect.fRight && rect.fTop <= rect.fBottom); + fBounds = rect; + fBoundsIsDirty = false; + fIsFinite = fBounds.isFinite(); + } + + /** Makes additional room but does not change the counts or change the genID */ + void incReserve(int additionalVerbs, int additionalPoints, int additionalConics) { + SkDEBUGCODE(this->validate();) + // Use reserve() so that if there is not enough space, the array will grow with some + // additional space. This ensures repeated calls to grow won't always allocate. + if (additionalPoints > 0) { + fPoints.reserve(fPoints.size() + additionalPoints); + } + if (additionalVerbs > 0) { + fVerbs.reserve(fVerbs.size() + additionalVerbs); + } + if (additionalConics > 0) { + fConicWeights.reserve(fConicWeights.size() + additionalConics); + } + SkDEBUGCODE(this->validate();) + } + + /** + * Resets all state except that of the verbs, points, and conic-weights. + * Intended to be called from other functions that reset state. + */ + void commonReset() { + SkDEBUGCODE(this->validate();) + this->callGenIDChangeListeners(); + fBoundsIsDirty = true; // this also invalidates fIsFinite + fGenerationID = 0; + + fSegmentMask = 0; + fType = PathType::kGeneral; + } + + /** Resets the path ref with verbCount verbs and pointCount points, all uninitialized. Also + * allocates space for reserveVerb additional verbs and reservePoints additional points.*/ + void resetToSize(int verbCount, int pointCount, int conicCount, + int reserveVerbs = 0, int reservePoints = 0, + int reserveConics = 0) { + this->commonReset(); + // Use reserve_exact() so the arrays are sized to exactly fit the data. + fPoints.reserve_exact(pointCount + reservePoints); + fPoints.resize_back(pointCount); + + fVerbs.reserve_exact(verbCount + reserveVerbs); + fVerbs.resize_back(verbCount); + + fConicWeights.reserve_exact(conicCount + reserveConics); + fConicWeights.resize_back(conicCount); + SkDEBUGCODE(this->validate();) + } + + /** + * Increases the verb count by numVbs and point count by the required amount. + * The new points are uninitialized. All the new verbs are set to the specified + * verb. If 'verb' is kConic_Verb, 'weights' will return a pointer to the + * uninitialized conic weights. + */ + SkPoint* growForRepeatedVerb(int /*SkPath::Verb*/ verb, int numVbs, SkScalar** weights); + + /** + * Increases the verb count 1, records the new verb, and creates room for the requisite number + * of additional points. A pointer to the first point is returned. Any new points are + * uninitialized. + */ + SkPoint* growForVerb(int /*SkPath::Verb*/ verb, SkScalar weight); + + /** + * Concatenates all verbs from 'path' onto our own verbs array. Increases the point count by the + * number of points in 'path', and the conic weight count by the number of conics in 'path'. + * + * Returns pointers to the uninitialized points and conic weights data. + */ + std::tuple growForVerbsInPath(const SkPathRef& path); + + /** + * Private, non-const-ptr version of the public function verbsMemBegin(). + */ + uint8_t* verbsBeginWritable() { return fVerbs.begin(); } + + /** + * Called the first time someone calls CreateEmpty to actually create the singleton. + */ + friend SkPathRef* sk_create_empty_pathref(); + + void setIsOval(bool isCCW, unsigned start, bool isClosed) { + fType = isClosed ? PathType::kOval : PathType::kOpenOval; + fRRectOrOvalIsCCW = isCCW; + fRRectOrOvalStartIdx = SkToU8(start); + } + + void setIsRRect(bool isCCW, unsigned start) { + fType = PathType::kRRect; + fRRectOrOvalIsCCW = isCCW; + fRRectOrOvalStartIdx = SkToU8(start); + } + + void setIsArc(const SkArc& arc) { + fType = PathType::kArc; + fArcOval = arc.fOval; + fArcStartAngle = arc.fStartAngle; + fArcSweepAngle = arc.fSweepAngle; + fArcType = arc.fType; + } + + // called only by the editor. Note that this is not a const function. + SkPoint* getWritablePoints() { + SkDEBUGCODE(this->validate();) + fType = PathType::kGeneral; + return fPoints.begin(); + } + + const SkPoint* getPoints() const { + SkDEBUGCODE(this->validate();) + return fPoints.begin(); + } + + void callGenIDChangeListeners(); + + mutable SkRect fBounds; + + enum { + kEmptyGenID = 1, // GenID reserved for path ref with zero points and zero verbs. + }; + mutable uint32_t fGenerationID; + SkIDChangeListener::List fGenIDChangeListeners; + + PointsArray fPoints; + VerbsArray fVerbs; + ConicWeightsArray fConicWeights; + + SkDEBUGCODE(std::atomic fEditorsAttached;) // assert only one editor in use at any time. + + mutable uint8_t fBoundsIsDirty; + mutable bool fIsFinite; // only meaningful if bounds are valid + + PathType fType; + // Both the circle and rrect special cases have a notion of direction and starting point + // The next two variables store that information for either. + bool fRRectOrOvalIsCCW; + uint8_t fRRectOrOvalStartIdx; + uint8_t fSegmentMask; + // If the path is an arc, these four variables store that information. + // We should just store an SkArc, but alignment would cost us 8 more bytes. + SkArc::Type fArcType; + SkRect fArcOval; + SkScalar fArcStartAngle; + SkScalar fArcSweepAngle; + + friend class PathRefTest_Private; + friend class ForceIsRRect_Private; // unit test isRRect + friend class SkPath; + friend class SkPathBuilder; + friend class SkPathPriv; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkSLSampleUsage.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkSLSampleUsage.h new file mode 100644 index 00000000000..39d9e258180 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkSLSampleUsage.h @@ -0,0 +1,85 @@ +/* + * Copyright 2020 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkSLSampleUsage_DEFINED +#define SkSLSampleUsage_DEFINED + +#include "include/core/SkTypes.h" + +namespace SkSL { + +/** + * Represents all of the ways that a fragment processor is sampled by its parent. + */ +class SampleUsage { +public: + enum class Kind { + // Child is never sampled + kNone, + // Child is only sampled at the same coordinates as the parent + kPassThrough, + // Child is sampled with a matrix whose value is uniform + kUniformMatrix, + // Child is sampled with sk_FragCoord.xy + kFragCoord, + // Child is sampled using explicit coordinates + kExplicit, + }; + + // Make a SampleUsage that corresponds to no sampling of the child at all + SampleUsage() = default; + + SampleUsage(Kind kind, bool hasPerspective) : fKind(kind), fHasPerspective(hasPerspective) { + if (kind != Kind::kUniformMatrix) { + SkASSERT(!fHasPerspective); + } + } + + // Child is sampled with a matrix whose value is uniform. The name is fixed. + static SampleUsage UniformMatrix(bool hasPerspective) { + return SampleUsage(Kind::kUniformMatrix, hasPerspective); + } + + static SampleUsage Explicit() { + return SampleUsage(Kind::kExplicit, false); + } + + static SampleUsage PassThrough() { + return SampleUsage(Kind::kPassThrough, false); + } + + static SampleUsage FragCoord() { return SampleUsage(Kind::kFragCoord, false); } + + bool operator==(const SampleUsage& that) const { + return fKind == that.fKind && fHasPerspective == that.fHasPerspective; + } + + bool operator!=(const SampleUsage& that) const { return !(*this == that); } + + // Arbitrary name used by all uniform sampling matrices + static const char* MatrixUniformName() { return "matrix"; } + + SampleUsage merge(const SampleUsage& other); + + Kind kind() const { return fKind; } + + bool hasPerspective() const { return fHasPerspective; } + + bool isSampled() const { return fKind != Kind::kNone; } + bool isPassThrough() const { return fKind == Kind::kPassThrough; } + bool isExplicit() const { return fKind == Kind::kExplicit; } + bool isUniformMatrix() const { return fKind == Kind::kUniformMatrix; } + bool isFragCoord() const { return fKind == Kind::kFragCoord; } + +private: + Kind fKind = Kind::kNone; + bool fHasPerspective = false; // Only valid if fKind is kUniformMatrix +}; + +} // namespace SkSL + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkWeakRefCnt.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkWeakRefCnt.h new file mode 100644 index 00000000000..4f949a48434 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkWeakRefCnt.h @@ -0,0 +1,173 @@ +/* + * Copyright 2012 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkWeakRefCnt_DEFINED +#define SkWeakRefCnt_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" + +#include +#include + +/** \class SkWeakRefCnt + + SkWeakRefCnt is the base class for objects that may be shared by multiple + objects. When an existing strong owner wants to share a reference, it calls + ref(). When a strong owner wants to release its reference, it calls + unref(). When the shared object's strong reference count goes to zero as + the result of an unref() call, its (virtual) weak_dispose method is called. + It is an error for the destructor to be called explicitly (or via the + object going out of scope on the stack or calling delete) if + getRefCnt() > 1. + + In addition to strong ownership, an owner may instead obtain a weak + reference by calling weak_ref(). A call to weak_ref() must be balanced by a + call to weak_unref(). To obtain a strong reference from a weak reference, + call try_ref(). If try_ref() returns true, the owner's pointer is now also + a strong reference on which unref() must be called. Note that this does not + affect the original weak reference, weak_unref() must still be called. When + the weak reference count goes to zero, the object is deleted. While the + weak reference count is positive and the strong reference count is zero the + object still exists, but will be in the disposed state. It is up to the + object to define what this means. + + Note that a strong reference implicitly implies a weak reference. As a + result, it is allowable for the owner of a strong ref to call try_ref(). + This will have the same effect as calling ref(), but may be more expensive. + + Example: + + SkWeakRefCnt myRef = strongRef.weak_ref(); + ... // strongRef.unref() may or may not be called + if (myRef.try_ref()) { + ... // use myRef + myRef.unref(); + } else { + // myRef is in the disposed state + } + myRef.weak_unref(); +*/ +class SK_API SkWeakRefCnt : public SkRefCnt { +public: + /** Default construct, initializing the reference counts to 1. + The strong references collectively hold one weak reference. When the + strong reference count goes to zero, the collectively held weak + reference is released. + */ + SkWeakRefCnt() : SkRefCnt(), fWeakCnt(1) {} + + /** Destruct, asserting that the weak reference count is 1. + */ + ~SkWeakRefCnt() override { +#ifdef SK_DEBUG + SkASSERT(getWeakCnt() == 1); + fWeakCnt.store(0, std::memory_order_relaxed); +#endif + } + +#ifdef SK_DEBUG + /** Return the weak reference count. */ + int32_t getWeakCnt() const { + return fWeakCnt.load(std::memory_order_relaxed); + } +#endif + +private: + /** If fRefCnt is 0, returns 0. + * Otherwise increments fRefCnt, acquires, and returns the old value. + */ + int32_t atomic_conditional_acquire_strong_ref() const { + int32_t prev = fRefCnt.load(std::memory_order_relaxed); + do { + if (0 == prev) { + break; + } + } while(!fRefCnt.compare_exchange_weak(prev, prev+1, std::memory_order_acquire, + std::memory_order_relaxed)); + return prev; + } + +public: + /** Creates a strong reference from a weak reference, if possible. The + caller must already be an owner. If try_ref() returns true the owner + is in posession of an additional strong reference. Both the original + reference and new reference must be properly unreferenced. If try_ref() + returns false, no strong reference could be created and the owner's + reference is in the same state as before the call. + */ + [[nodiscard]] bool try_ref() const { + if (atomic_conditional_acquire_strong_ref() != 0) { + // Acquire barrier (L/SL), if not provided above. + // Prevents subsequent code from happening before the increment. + return true; + } + return false; + } + + /** Increment the weak reference count. Must be balanced by a call to + weak_unref(). + */ + void weak_ref() const { + SkASSERT(getRefCnt() > 0); + SkASSERT(getWeakCnt() > 0); + // No barrier required. + (void)fWeakCnt.fetch_add(+1, std::memory_order_relaxed); + } + + /** Decrement the weak reference count. If the weak reference count is 1 + before the decrement, then call delete on the object. Note that if this + is the case, then the object needs to have been allocated via new, and + not on the stack. + */ + void weak_unref() const { + SkASSERT(getWeakCnt() > 0); + // A release here acts in place of all releases we "should" have been doing in ref(). + if (1 == fWeakCnt.fetch_add(-1, std::memory_order_acq_rel)) { + // Like try_ref(), the acquire is only needed on success, to make sure + // code in internal_dispose() doesn't happen before the decrement. +#ifdef SK_DEBUG + // so our destructor won't complain + fWeakCnt.store(1, std::memory_order_relaxed); +#endif + this->INHERITED::internal_dispose(); + } + } + + /** Returns true if there are no strong references to the object. When this + is the case all future calls to try_ref() will return false. + */ + bool weak_expired() const { + return fRefCnt.load(std::memory_order_relaxed) == 0; + } + +protected: + /** Called when the strong reference count goes to zero. This allows the + object to free any resources it may be holding. Weak references may + still exist and their level of allowed access to the object is defined + by the object's class. + */ + virtual void weak_dispose() const { + } + +private: + /** Called when the strong reference count goes to zero. Calls weak_dispose + on the object and releases the implicit weak reference held + collectively by the strong references. + */ + void internal_dispose() const override { + weak_dispose(); + weak_unref(); + } + + /* Invariant: fWeakCnt = #weak + (fRefCnt > 0 ? 1 : 0) */ + mutable std::atomic fWeakCnt; + + using INHERITED = SkRefCnt; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkXmp.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkXmp.h new file mode 100644 index 00000000000..58ae2338d62 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/SkXmp.h @@ -0,0 +1,62 @@ +/* + * Copyright 2023 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkXmp_DEFINED +#define SkXmp_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +class SkData; +struct SkGainmapInfo; + +#include +#include + +/* + * An interface to extract information from XMP metadata. + */ +class SK_API SkXmp { +public: + SkXmp() = default; + virtual ~SkXmp() = default; + // Make noncopyable + SkXmp(const SkXmp&) = delete; + SkXmp& operator= (const SkXmp&) = delete; + + // Create from XMP data. + static std::unique_ptr Make(sk_sp xmpData); + // Create from standard XMP + extended XMP data, see XMP Specification Part 3: Storage in files, + // Section 1.1.3.1: Extended XMP in JPEG + static std::unique_ptr Make(sk_sp xmpStandard, sk_sp xmpExtended); + + // Extract HDRGM gainmap parameters. + // TODO(b/338342146): Remove this once all callers are removed. + bool getGainmapInfoHDRGM(SkGainmapInfo* info) const { return getGainmapInfoAdobe(info); } + + // Extract gainmap parameters from http://ns.adobe.com/hdr-gain-map/1.0/. + virtual bool getGainmapInfoAdobe(SkGainmapInfo* info) const = 0; + + // If the image specifies http://ns.apple.com/pixeldatainfo/1.0/ AuxiliaryImageType of + // urn:com:apple:photo:2020:aux:hdrgainmap, and includes a http://ns.apple.com/HDRGainMap/1.0/ + // HDRGainMapVersion, then populate |info| with gainmap parameters that will approximate the + // math specified at [0] and return true. + // [0] https://developer.apple.com/documentation/appkit/images_and_pdf/ + // applying_apple_hdr_effect_to_your_photos + virtual bool getGainmapInfoApple(float exifHdrHeadroom, SkGainmapInfo* info) const = 0; + + // If this includes GContainer metadata and the GContainer contains an item with semantic + // GainMap and Mime of image/jpeg, then return true, and populate |offset| and |size| with + // that item's offset (from the end of the primary JPEG image's EndOfImage), and the size of + // the gainmap. + virtual bool getContainerGainmapLocation(size_t* offset, size_t* size) const = 0; + + // Return the GUID of an Extended XMP if present, or null otherwise. + virtual const char* getExtendedXmpGuid() const = 0; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/README.md b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/README.md new file mode 100644 index 00000000000..7f4f17b228c --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/README.md @@ -0,0 +1,4 @@ +Files in "base" are used by many parts of Skia, but are not part of the public Skia API. +See also src/base for other files that are part of base, but not needed by the public API. + +Files here should not depend on anything other than system headers or other files in base. \ No newline at end of file diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SingleOwner.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SingleOwner.h new file mode 100644 index 00000000000..473981e1fb8 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SingleOwner.h @@ -0,0 +1,75 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef skgpu_SingleOwner_DEFINED +#define skgpu_SingleOwner_DEFINED + +#include "include/private/base/SkDebug.h" // IWYU pragma: keep + +#if defined(SK_DEBUG) +#include "include/private/base/SkAssert.h" +#include "include/private/base/SkMutex.h" +#include "include/private/base/SkThreadAnnotations.h" +#include "include/private/base/SkThreadID.h" + +#endif + +namespace skgpu { + +#if defined(SK_DEBUG) + +#define SKGPU_ASSERT_SINGLE_OWNER(obj) \ + skgpu::SingleOwner::AutoEnforce debug_SingleOwner(obj, __FILE__, __LINE__); + +// This is a debug tool to verify an object is only being used from one thread at a time. +class SingleOwner { +public: + SingleOwner() : fOwner(kIllegalThreadID), fReentranceCount(0) {} + + struct AutoEnforce { + AutoEnforce(SingleOwner* so, const char* file, int line) + : fFile(file), fLine(line), fSO(so) { + fSO->enter(file, line); + } + ~AutoEnforce() { fSO->exit(fFile, fLine); } + + const char* fFile; + int fLine; + SingleOwner* fSO; + }; + +private: + void enter(const char* file, int line) { + SkAutoMutexExclusive lock(fMutex); + SkThreadID self = SkGetThreadID(); + SkASSERTF(fOwner == self || fOwner == kIllegalThreadID, "%s:%d Single owner failure.", + file, line); + fReentranceCount++; + fOwner = self; + } + + void exit(const char* file, int line) { + SkAutoMutexExclusive lock(fMutex); + SkASSERTF(fOwner == SkGetThreadID(), "%s:%d Single owner failure.", file, line); + fReentranceCount--; + if (fReentranceCount == 0) { + fOwner = kIllegalThreadID; + } + } + + SkMutex fMutex; + SkThreadID fOwner SK_GUARDED_BY(fMutex); + int fReentranceCount SK_GUARDED_BY(fMutex); +}; +#else +#define SKGPU_ASSERT_SINGLE_OWNER(obj) +class SingleOwner {}; // Provide a no-op implementation so we can pass pointers to constructors +#endif + +} // namespace skgpu + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAPI.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAPI.h new file mode 100644 index 00000000000..4028f95d87d --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAPI.h @@ -0,0 +1,52 @@ +/* + * Copyright 2022 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkAPI_DEFINED +#define SkAPI_DEFINED + +#include "include/private/base/SkLoadUserConfig.h" // IWYU pragma: keep + +// If SKIA_IMPLEMENTATION is defined as 1, that signals we are building Skia and should +// export our symbols. If it is not set (or set to 0), then Skia is being used by a client +// and we should not export our symbols. +#if !defined(SKIA_IMPLEMENTATION) + #define SKIA_IMPLEMENTATION 0 +#endif + +// If we are compiling Skia is being as a DLL, we need to be sure to export all of our public +// APIs to that DLL. If a client is using Skia which was compiled as a DLL, we need to instruct +// the linker to use the symbols from that DLL. This is the goal of the SK_API define. +#if !defined(SK_API) + #if defined(SKIA_DLL) + #if defined(_MSC_VER) + #if SKIA_IMPLEMENTATION + #define SK_API __declspec(dllexport) + #else + #define SK_API __declspec(dllimport) + #endif + #else + #define SK_API __attribute__((visibility("default"))) + #endif + #else + #define SK_API + #endif +#endif + +// SK_SPI is functionally identical to SK_API, but used within src to clarify that it's less stable +#if !defined(SK_SPI) + #define SK_SPI SK_API +#endif + +// See https://clang.llvm.org/docs/AttributeReference.html#availability +// The API_AVAILABLE macro comes from on MacOS +#if defined(SK_ENABLE_API_AVAILABLE) +# define SK_API_AVAILABLE API_AVAILABLE +#else +# define SK_API_AVAILABLE(...) +#endif + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkASAN.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkASAN.h new file mode 100644 index 00000000000..095f71608d2 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkASAN.h @@ -0,0 +1,56 @@ +/* + * Copyright 2020 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkASAN_DEFINED +#define SkASAN_DEFINED + +#include + +#ifdef __SANITIZE_ADDRESS__ + #define SK_SANITIZE_ADDRESS 1 +#endif +#if !defined(SK_SANITIZE_ADDRESS) && defined(__has_feature) + #if __has_feature(address_sanitizer) + #define SK_SANITIZE_ADDRESS 1 + #endif +#endif + +// Typically declared in LLVM's asan_interface.h. +#ifdef SK_SANITIZE_ADDRESS +extern "C" { + void __asan_poison_memory_region(void const volatile *addr, size_t size); + void __asan_unpoison_memory_region(void const volatile *addr, size_t size); + int __asan_address_is_poisoned(void const volatile *addr); +} +#endif + +// Code that implements bespoke allocation arenas can poison the entire arena on creation, then +// unpoison chunks of arena memory as they are parceled out. Consider leaving gaps between blocks +// to detect buffer overrun. +static inline void sk_asan_poison_memory_region([[maybe_unused]] void const volatile* addr, + [[maybe_unused]] size_t size) { +#ifdef SK_SANITIZE_ADDRESS + __asan_poison_memory_region(addr, size); +#endif +} + +static inline void sk_asan_unpoison_memory_region([[maybe_unused]] void const volatile* addr, + [[maybe_unused]] size_t size) { +#ifdef SK_SANITIZE_ADDRESS + __asan_unpoison_memory_region(addr, size); +#endif +} + +static inline int sk_asan_address_is_poisoned([[maybe_unused]] void const volatile* addr) { +#ifdef SK_SANITIZE_ADDRESS + return __asan_address_is_poisoned(addr); +#else + return 0; +#endif +} + +#endif // SkASAN_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAlign.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAlign.h new file mode 100644 index 00000000000..2b2138ddd49 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAlign.h @@ -0,0 +1,39 @@ +/* + * Copyright 2022 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkAlign_DEFINED +#define SkAlign_DEFINED + +#include "include/private/base/SkAssert.h" + +#include + +template static constexpr T SkAlign2(T x) { return (x + 1) >> 1 << 1; } +template static constexpr T SkAlign4(T x) { return (x + 3) >> 2 << 2; } +template static constexpr T SkAlign8(T x) { return (x + 7) >> 3 << 3; } + +template static constexpr bool SkIsAlign2(T x) { return 0 == (x & 1); } +template static constexpr bool SkIsAlign4(T x) { return 0 == (x & 3); } +template static constexpr bool SkIsAlign8(T x) { return 0 == (x & 7); } + +template static constexpr T SkAlignPtr(T x) { + return sizeof(void*) == 8 ? SkAlign8(x) : SkAlign4(x); +} +template static constexpr bool SkIsAlignPtr(T x) { + return sizeof(void*) == 8 ? SkIsAlign8(x) : SkIsAlign4(x); +} + +/** + * align up to a power of 2 + */ +static inline constexpr size_t SkAlignTo(size_t x, size_t alignment) { + // The same as alignment && SkIsPow2(value), w/o a dependency cycle. + SkASSERT(alignment && (alignment & (alignment - 1)) == 0); + return (x + alignment - 1) & ~(alignment - 1); +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAlignedStorage.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAlignedStorage.h new file mode 100644 index 00000000000..532ad03978f --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAlignedStorage.h @@ -0,0 +1,32 @@ +// Copyright 2022 Google LLC +// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +#ifndef SkAlignedStorage_DEFINED +#define SkAlignedStorage_DEFINED + +#include +#include + +template class SkAlignedSTStorage { +public: + SkAlignedSTStorage() {} + SkAlignedSTStorage(SkAlignedSTStorage&&) = delete; + SkAlignedSTStorage(const SkAlignedSTStorage&) = delete; + SkAlignedSTStorage& operator=(SkAlignedSTStorage&&) = delete; + SkAlignedSTStorage& operator=(const SkAlignedSTStorage&) = delete; + + // Returns void* because this object does not initialize the + // memory. Use placement new for types that require a constructor. + void* get() { return fStorage; } + const void* get() const { return fStorage; } + + // Act as a container of bytes because the storage is uninitialized. + std::byte* data() { return fStorage; } + const std::byte* data() const { return fStorage; } + size_t size() const { return std::size(fStorage); } + +private: + alignas(T) std::byte fStorage[sizeof(T) * N]; +}; + +#endif // SkAlignedStorage_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAnySubclass.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAnySubclass.h new file mode 100644 index 00000000000..2b666cbdb17 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAnySubclass.h @@ -0,0 +1,73 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkAnySubclass_DEFINED +#define SkAnySubclass_DEFINED + +#include "include/private/base/SkAssert.h" + +#include +#include +#include // IWYU pragma: keep +#include + +/** + * Stores any subclass `T` of `Base`, where sizeof(T) <= `Size`, without using the heap. + * Doesn't need advance knowledge of T, so it's particularly suited to platform or backend + * implementations of a generic interface, where the set of possible subclasses is finite and + * known, but can't be made available at compile-time. + */ +template +class SkAnySubclass { +public: + SkAnySubclass() = default; + ~SkAnySubclass() { + this->reset(); + } + + SkAnySubclass(const SkAnySubclass&) = delete; + SkAnySubclass& operator=(const SkAnySubclass&) = delete; + SkAnySubclass(SkAnySubclass&&) = delete; + SkAnySubclass& operator=(SkAnySubclass&&) = delete; + + template + void emplace(Args&&... args) { + static_assert(std::is_base_of_v); + static_assert(sizeof(T) <= Size); + // We're going to clean up our stored object by calling ~Base: + static_assert(std::has_virtual_destructor_v || std::is_trivially_destructible_v); + SkASSERT(!fValid); + new (fData) T(std::forward(args)...); + fValid = true; + } + + void reset() { + if (fValid) { + this->get()->~Base(); + } + fValid = false; + } + + const Base* get() const { + SkASSERT(fValid); + return std::launder(reinterpret_cast(fData)); + } + + Base* get() { + SkASSERT(fValid); + return std::launder(reinterpret_cast(fData)); + } + + Base* operator->() { return this->get(); } + const Base* operator->() const { return this->get(); } + +private: + alignas(8) std::byte fData[Size]; + bool fValid = false; +}; + +#endif // SkAnySubclass_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAssert.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAssert.h new file mode 100644 index 00000000000..67b31213dd5 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAssert.h @@ -0,0 +1,202 @@ +/* + * Copyright 2022 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkAssert_DEFINED +#define SkAssert_DEFINED + +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkAttributes.h" +#include "include/private/base/SkDebug.h" // IWYU pragma: keep + +#include +#include + +#if defined(__clang__) && defined(__has_attribute) + #if __has_attribute(likely) + #define SK_LIKELY [[likely]] + #define SK_UNLIKELY [[unlikely]] + #else + #define SK_LIKELY + #define SK_UNLIKELY + #endif +#else + #define SK_LIKELY + #define SK_UNLIKELY +#endif + +// c++23 will give us [[assume]] -- until then we're stuck with various other options: +#if defined(__clang__) + #define SK_ASSUME(cond) __builtin_assume(cond) +#elif defined(__GNUC__) + #if __GNUC__ >= 13 + #define SK_ASSUME(cond) __attribute__((assume(cond))) + #else + // NOTE: This implementation could actually evaluate `cond`, which is not desirable. + #define SK_ASSUME(cond) ((cond) ? (void)0 : __builtin_unreachable()) + #endif +#elif defined(_MSC_VER) + #define SK_ASSUME(cond) __assume(cond) +#else + #define SK_ASSUME(cond) ((void)0) +#endif + +/** Called internally if we hit an unrecoverable error. + The platform implementation must not return, but should either throw + an exception or otherwise exit. +*/ +[[noreturn]] SK_API extern void sk_abort_no_print(void); + +#if defined(SK_BUILD_FOR_GOOGLE3) + void SkDebugfForDumpStackTrace(const char* data, void* unused); + namespace base { + void DumpStackTrace(int skip_count, void w(const char*, void*), void* arg); + } +# define SK_DUMP_GOOGLE3_STACK() ::base::DumpStackTrace(0, SkDebugfForDumpStackTrace, nullptr) +#else +# define SK_DUMP_GOOGLE3_STACK() +#endif + +#if !defined(SK_ABORT) +# if defined(SK_BUILD_FOR_WIN) + // This style lets Visual Studio follow errors back to the source file. +# define SK_DUMP_LINE_FORMAT "%s(%d)" +# else +# define SK_DUMP_LINE_FORMAT "%s:%d" +# endif +# define SK_ABORT(message, ...) \ + do { \ + SkDebugf(SK_DUMP_LINE_FORMAT ": fatal error: \"" message "\"\n", \ + __FILE__, __LINE__, ##__VA_ARGS__); \ + SK_DUMP_GOOGLE3_STACK(); \ + sk_abort_no_print(); \ + } while (false) +#endif + +// SkASSERT, SkASSERTF and SkASSERT_RELEASE can be used as standalone assertion expressions, e.g. +// uint32_t foo(int x) { +// SkASSERT(x > 4); +// return x - 4; +// } +// and are also written to be compatible with constexpr functions: +// constexpr uint32_t foo(int x) { +// return SkASSERT(x > 4), +// x - 4; +// } +#if defined(__clang__) +#define SkASSERT_RELEASE(cond) \ + static_cast( __builtin_expect(static_cast(cond), 1) \ + ? static_cast(0) \ + : []{ SK_ABORT("check(%s)", #cond); }() ) + +#define SkASSERTF_RELEASE(cond, fmt, ...) \ + static_cast( __builtin_expect(static_cast(cond), 1) \ + ? static_cast(0) \ + : [&]{ SK_ABORT("assertf(%s): " fmt, #cond, ##__VA_ARGS__); }() ) +#else +#define SkASSERT_RELEASE(cond) \ + static_cast( (cond) ? static_cast(0) : []{ SK_ABORT("check(%s)", #cond); }() ) + +#define SkASSERTF_RELEASE(cond, fmt, ...) \ + static_cast( (cond) \ + ? static_cast(0) \ + : [&]{ SK_ABORT("assertf(%s): " fmt, #cond, ##__VA_ARGS__); }() ) +#endif + +#if defined(SK_DEBUG) + #define SkASSERT(cond) SkASSERT_RELEASE(cond) + #define SkASSERTF(cond, fmt, ...) SkASSERTF_RELEASE(cond, fmt, ##__VA_ARGS__) + #define SkDEBUGFAIL(message) SK_ABORT("%s", message) + #define SkDEBUGFAILF(fmt, ...) SK_ABORT(fmt, ##__VA_ARGS__) + #define SkAssertResult(cond) SkASSERT(cond) +#else + #define SkASSERT(cond) static_cast(0) + #define SkASSERTF(cond, fmt, ...) static_cast(0) + #define SkDEBUGFAIL(message) + #define SkDEBUGFAILF(fmt, ...) + + // unlike SkASSERT, this macro executes its condition in the non-debug build. + // The if is present so that this can be used with functions marked [[nodiscard]]. + #define SkAssertResult(cond) if (cond) {} do {} while(false) +#endif + +#if !defined(SkUNREACHABLE) +# if defined(_MSC_VER) && !defined(__clang__) +# include +# define FAST_FAIL_INVALID_ARG 5 +// See https://developercommunity.visualstudio.com/content/problem/1128631/code-flow-doesnt-see-noreturn-with-extern-c.html +// for why this is wrapped. Hopefully removable after msvc++ 19.27 is no longer supported. +[[noreturn]] static inline void sk_fast_fail() { __fastfail(FAST_FAIL_INVALID_ARG); } +# define SkUNREACHABLE sk_fast_fail() +# else +# define SkUNREACHABLE __builtin_trap() +# endif +#endif + +[[noreturn]] SK_API inline void sk_print_index_out_of_bounds(size_t i, size_t size) { + SK_ABORT("Index (%zu) out of bounds for size %zu.\n", i, size); +} + +template SK_API inline T sk_collection_check_bounds(T i, T size) { + if (0 <= i && i < size) SK_LIKELY { + return i; + } + + SK_UNLIKELY { + #if defined(SK_DEBUG) + sk_print_index_out_of_bounds(static_cast(i), static_cast(size)); + #else + SkUNREACHABLE; + #endif + } +} + +[[noreturn]] SK_API inline void sk_print_length_too_big(size_t i, size_t size) { + SK_ABORT("Length (%zu) is too big for size %zu.\n", i, size); +} + +template SK_API inline T sk_collection_check_length(T i, T size) { + if (0 <= i && i <= size) SK_LIKELY { + return i; + } + + SK_UNLIKELY { + #if defined(SK_DEBUG) + sk_print_length_too_big(static_cast(i), static_cast(size)); + #else + SkUNREACHABLE; + #endif + } +} + +SK_API inline void sk_collection_not_empty(bool empty) { + if (empty) SK_UNLIKELY { + #if defined(SK_DEBUG) + SK_ABORT("Collection is empty.\n"); + #else + SkUNREACHABLE; + #endif + } +} + +[[noreturn]] SK_API inline void sk_print_size_too_big(size_t size, size_t maxSize) { + SK_ABORT("Size (%zu) can't be represented in bytes. Max size is %zu.\n", size, maxSize); +} + +template +SK_ALWAYS_INLINE size_t check_size_bytes_too_big(size_t size) { + const size_t kMaxSize = std::numeric_limits::max() / sizeof(T); + if (size > kMaxSize) { + #if defined(SK_DEBUG) + sk_print_size_too_big(size, kMaxSize); + #else + SkUNREACHABLE; + #endif + } + return size; +} + +#endif // SkAssert_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAttributes.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAttributes.h new file mode 100644 index 00000000000..f8df5905cde --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkAttributes.h @@ -0,0 +1,90 @@ +/* + * Copyright 2022 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkAttributes_DEFINED +#define SkAttributes_DEFINED + +#include "include/private/base/SkFeatures.h" // IWYU pragma: keep +#include "include/private/base/SkLoadUserConfig.h" // IWYU pragma: keep + +#if defined(__clang__) || defined(__GNUC__) +# define SK_ATTRIBUTE(attr) __attribute__((attr)) +#else +# define SK_ATTRIBUTE(attr) +#endif + +/** + * If your judgment is better than the compiler's (i.e. you've profiled it), + * you can use SK_ALWAYS_INLINE to force inlining. E.g. + * inline void someMethod() { ... } // may not be inlined + * SK_ALWAYS_INLINE void someMethod() { ... } // should always be inlined + */ +#if !defined(SK_ALWAYS_INLINE) +# if defined(SK_BUILD_FOR_WIN) +# define SK_ALWAYS_INLINE __forceinline +# else +# define SK_ALWAYS_INLINE SK_ATTRIBUTE(always_inline) inline +# endif +#endif + +/** + * If your judgment is better than the compiler's (i.e. you've profiled it), + * you can use SK_NEVER_INLINE to prevent inlining. + */ +#if !defined(SK_NEVER_INLINE) +# if defined(SK_BUILD_FOR_WIN) +# define SK_NEVER_INLINE __declspec(noinline) +# else +# define SK_NEVER_INLINE SK_ATTRIBUTE(noinline) +# endif +#endif + +/** + * Used to annotate a function as taking printf style arguments. + * `A` is the (1 based) index of the format string argument. + * `B` is the (1 based) index of the first argument used by the format string. + */ +#if !defined(SK_PRINTF_LIKE) +# define SK_PRINTF_LIKE(A, B) SK_ATTRIBUTE(format(printf, (A), (B))) +#endif + +/** + * Used to ignore sanitizer warnings. + */ +#if !defined(SK_NO_SANITIZE) +# define SK_NO_SANITIZE(A) SK_ATTRIBUTE(no_sanitize(A)) +#endif + +/** + * Helper macro to define no_sanitize attributes only with clang. + */ +#if defined(__clang__) && defined(__has_attribute) + #if __has_attribute(no_sanitize) + #define SK_CLANG_NO_SANITIZE(A) SK_NO_SANITIZE(A) + #endif +#endif + +#if !defined(SK_CLANG_NO_SANITIZE) + #define SK_CLANG_NO_SANITIZE(A) +#endif + +/** + * Annotates a class' non-trivial special functions as trivial for the purposes of calls. + * Allows a class with a non-trivial destructor to be __is_trivially_relocatable. + * Use of this attribute on a public API breaks platform ABI. + * Annotated classes may not hold pointers derived from `this`. + * Annotated classes must implement move+delete as equivalent to memcpy+free. + * Use may require more complete types, as callee destroys. + * + * https://clang.llvm.org/docs/AttributeReference.html#trivial-abi + * https://libcxx.llvm.org/DesignDocs/UniquePtrTrivialAbi.html + */ +#if !defined(SK_TRIVIAL_ABI) +# define SK_TRIVIAL_ABI +#endif + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkCPUTypes.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkCPUTypes.h new file mode 100644 index 00000000000..a5f60fd3ef1 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkCPUTypes.h @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkCPUTypes_DEFINED +#define SkCPUTypes_DEFINED + +// TODO(bungeman,kjlubick) There are a lot of assumptions throughout the codebase that +// these types are 32 bits, when they could be more or less. Public APIs should stop +// using these. Internally, we could use uint_fast8_t and uint_fast16_t, but not in +// public APIs due to ABI incompatibilities. + +/** Fast type for unsigned 8 bits. Use for parameter passing and local + variables, not for storage +*/ +typedef unsigned U8CPU; + +/** Fast type for unsigned 16 bits. Use for parameter passing and local + variables, not for storage +*/ +typedef unsigned U16CPU; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkContainers.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkContainers.h new file mode 100644 index 00000000000..587e295efac --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkContainers.h @@ -0,0 +1,54 @@ +// Copyright 2022 Google LLC. +// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +#ifndef SkContainers_DEFINED +#define SkContainers_DEFINED + +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkAlign.h" +#include "include/private/base/SkSpan_impl.h" + +#include +#include + +class SK_SPI SkContainerAllocator { +public: + SkContainerAllocator(size_t sizeOfT, int maxCapacity) + : fSizeOfT{sizeOfT} + , fMaxCapacity{maxCapacity} {} + + // allocate will abort on failure. Given a capacity of 0, it will return the empty span. + // The bytes allocated are freed using sk_free(). + SkSpan allocate(int capacity, double growthFactor = 1.0); + + // Rounds a requested capacity up towards `kCapacityMultiple` in a constexpr-friendly fashion. + template + static constexpr size_t RoundUp(size_t capacity) { + return SkAlignTo(capacity * sizeof(T), kCapacityMultiple) / sizeof(T); + } + +private: + friend struct SkContainerAllocatorTestingPeer; + + // All capacity counts will be rounded up to kCapacityMultiple. This matches ASAN's shadow + // granularity, as well as our typical struct alignment on a 64-bit machine. + static constexpr int64_t kCapacityMultiple = 8; + + // Rounds up capacity to next multiple of kCapacityMultiple and pin to fMaxCapacity. + size_t roundUpCapacity(int64_t capacity) const; + + // Grows the capacity by growthFactor being sure to stay with in kMinBytes and fMaxCapacity. + size_t growthFactorCapacity(int capacity, double growthFactor) const; + + const size_t fSizeOfT; + const int64_t fMaxCapacity; +}; + +// sk_allocate_canfail returns the empty span on failure. Parameter size must be > 0. +SkSpan sk_allocate_canfail(size_t size); + +// Returns the empty span if size is 0. sk_allocate_throw aborts on failure. +SkSpan sk_allocate_throw(size_t size); + +SK_SPI void sk_report_container_overflow_and_die(); +#endif // SkContainers_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkDebug.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkDebug.h new file mode 100644 index 00000000000..2e4810fc1c9 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkDebug.h @@ -0,0 +1,27 @@ +/* + * Copyright 2022 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkDebug_DEFINED +#define SkDebug_DEFINED + +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkAttributes.h" +#include "include/private/base/SkLoadUserConfig.h" // IWYU pragma: keep + +#if !defined(SkDebugf) + void SK_SPI SkDebugf(const char format[], ...) SK_PRINTF_LIKE(1, 2); +#endif + +#if defined(SK_DEBUG) + #define SkDEBUGCODE(...) __VA_ARGS__ + #define SkDEBUGF(...) SkDebugf(__VA_ARGS__) +#else + #define SkDEBUGCODE(...) + #define SkDEBUGF(...) +#endif + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkDeque.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkDeque.h new file mode 100644 index 00000000000..c7a43c9fe46 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkDeque.h @@ -0,0 +1,138 @@ + +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + + +#ifndef SkDeque_DEFINED +#define SkDeque_DEFINED + +#include "include/private/base/SkAPI.h" + +#include + +/* + * The deque class works by blindly creating memory space of a specified element + * size. It manages the memory as a doubly linked list of blocks each of which + * can contain multiple elements. Pushes and pops add/remove blocks from the + * beginning/end of the list as necessary while each block tracks the used + * portion of its memory. + * One behavior to be aware of is that the pops do not immediately remove an + * empty block from the beginning/end of the list (Presumably so push/pop pairs + * on the block boundaries don't cause thrashing). This can result in the first/ + * last element not residing in the first/last block. + */ +class SK_API SkDeque { +public: + /** + * elemSize specifies the size of each individual element in the deque + * allocCount specifies how many elements are to be allocated as a block + */ + explicit SkDeque(size_t elemSize, int allocCount = 1); + SkDeque(size_t elemSize, void* storage, size_t storageSize, int allocCount = 1); + ~SkDeque(); + + bool empty() const { return 0 == fCount; } + int count() const { return fCount; } + size_t elemSize() const { return fElemSize; } + + const void* front() const { return fFront; } + const void* back() const { return fBack; } + + void* front() { return fFront; } + void* back() { return fBack; } + + /** + * push_front and push_back return a pointer to the memory space + * for the new element + */ + void* push_front(); + void* push_back(); + + void pop_front(); + void pop_back(); + +private: + struct Block; + +public: + class Iter { + public: + enum IterStart { + kFront_IterStart, + kBack_IterStart, + }; + + /** + * Creates an uninitialized iterator. Must be reset() + */ + Iter(); + + Iter(const SkDeque& d, IterStart startLoc); + void* next(); + void* prev(); + + void reset(const SkDeque& d, IterStart startLoc); + + private: + SkDeque::Block* fCurBlock; + char* fPos; + size_t fElemSize; + }; + + // Inherit privately from Iter to prevent access to reverse iteration + class F2BIter : private Iter { + public: + F2BIter() {} + + /** + * Wrap Iter's 2 parameter ctor to force initialization to the + * beginning of the deque + */ + F2BIter(const SkDeque& d) : INHERITED(d, kFront_IterStart) {} + + using Iter::next; + + /** + * Wrap Iter::reset to force initialization to the beginning of the + * deque + */ + void reset(const SkDeque& d) { + this->INHERITED::reset(d, kFront_IterStart); + } + + private: + using INHERITED = Iter; + }; + +private: + // allow unit test to call numBlocksAllocated + friend class DequeUnitTestHelper; + + void* fFront; + void* fBack; + + Block* fFrontBlock; + Block* fBackBlock; + size_t fElemSize; + void* fInitialStorage; + int fCount; // number of elements in the deque + int fAllocCount; // number of elements to allocate per block + + Block* allocateBlock(int allocCount); + void freeBlock(Block* block); + + /** + * This returns the number of chunk blocks allocated by the deque. It + * can be used to gauge the effectiveness of the selected allocCount. + */ + int numBlocksAllocated() const; + + SkDeque(const SkDeque&) = delete; + SkDeque& operator=(const SkDeque&) = delete; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkFeatures.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkFeatures.h new file mode 100644 index 00000000000..353ce22897f --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkFeatures.h @@ -0,0 +1,165 @@ +/* + * Copyright 2022 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFeatures_DEFINED +#define SkFeatures_DEFINED + +#if !defined(SK_BUILD_FOR_ANDROID) && !defined(SK_BUILD_FOR_IOS) && !defined(SK_BUILD_FOR_WIN) && \ + !defined(SK_BUILD_FOR_UNIX) && !defined(SK_BUILD_FOR_MAC) + + #ifdef __APPLE__ + #include + #endif + + #if defined(_WIN32) || defined(__SYMBIAN32__) + #define SK_BUILD_FOR_WIN + #elif defined(ANDROID) || defined(__ANDROID__) + #define SK_BUILD_FOR_ANDROID + #elif defined(linux) || defined(__linux) || defined(__FreeBSD__) || \ + defined(__OpenBSD__) || defined(__sun) || defined(__NetBSD__) || \ + defined(__DragonFly__) || defined(__Fuchsia__) || \ + defined(__GLIBC__) || defined(__GNU__) || defined(__unix__) + #define SK_BUILD_FOR_UNIX + #elif TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + #define SK_BUILD_FOR_IOS + #else + #define SK_BUILD_FOR_MAC + #endif +#endif // end SK_BUILD_FOR_* + + +#if defined(SK_BUILD_FOR_WIN) && !defined(__clang__) + #if !defined(SK_RESTRICT) + #define SK_RESTRICT __restrict + #endif +#endif + +#if !defined(SK_RESTRICT) + #define SK_RESTRICT __restrict__ +#endif + +#if !defined(SK_CPU_BENDIAN) && !defined(SK_CPU_LENDIAN) + #if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) + #define SK_CPU_BENDIAN + #elif defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) + #define SK_CPU_LENDIAN + #elif defined(__sparc) || defined(__sparc__) || \ + defined(_POWER) || defined(__powerpc__) || \ + defined(__ppc__) || defined(__hppa) || \ + defined(__PPC__) || defined(__PPC64__) || \ + defined(_MIPSEB) || defined(__ARMEB__) || \ + defined(__s390__) || \ + (defined(__sh__) && defined(__BIG_ENDIAN__)) || \ + (defined(__ia64) && defined(__BIG_ENDIAN__)) + #define SK_CPU_BENDIAN + #else + #define SK_CPU_LENDIAN + #endif +#endif + +#if defined(__i386) || defined(_M_IX86) || defined(__x86_64__) || defined(_M_X64) + #define SK_CPU_X86 1 +#endif + +#if defined(__loongarch__) || defined (__loongarch64) + #define SK_CPU_LOONGARCH 1 +#endif + +/** + * SK_CPU_SSE_LEVEL + * + * If defined, SK_CPU_SSE_LEVEL should be set to the highest supported level. + * On non-intel CPU this should be undefined. + */ +#define SK_CPU_SSE_LEVEL_SSE1 10 +#define SK_CPU_SSE_LEVEL_SSE2 20 +#define SK_CPU_SSE_LEVEL_SSE3 30 +#define SK_CPU_SSE_LEVEL_SSSE3 31 +#define SK_CPU_SSE_LEVEL_SSE41 41 +#define SK_CPU_SSE_LEVEL_SSE42 42 +#define SK_CPU_SSE_LEVEL_AVX 51 +#define SK_CPU_SSE_LEVEL_AVX2 52 +#define SK_CPU_SSE_LEVEL_SKX 60 + +/** + * SK_CPU_LSX_LEVEL + * + * If defined, SK_CPU_LSX_LEVEL should be set to the highest supported level. + * On non-loongarch CPU this should be undefined. + */ +#define SK_CPU_LSX_LEVEL_LSX 70 +#define SK_CPU_LSX_LEVEL_LASX 80 + +// TODO(brianosman,kjlubick) clean up these checks + +// Are we in GCC/Clang? +#ifndef SK_CPU_SSE_LEVEL + // These checks must be done in descending order to ensure we set the highest + // available SSE level. + #if defined(__AVX512F__) && defined(__AVX512DQ__) && defined(__AVX512CD__) && \ + defined(__AVX512BW__) && defined(__AVX512VL__) + #define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SKX + #elif defined(__AVX2__) + #define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_AVX2 + #elif defined(__AVX__) + #define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_AVX + #elif defined(__SSE4_2__) + #define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SSE42 + #elif defined(__SSE4_1__) + #define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SSE41 + #elif defined(__SSSE3__) + #define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SSSE3 + #elif defined(__SSE3__) + #define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SSE3 + #elif defined(__SSE2__) + #define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SSE2 + #endif +#endif + +#ifndef SK_CPU_LSX_LEVEL + #if defined(__loongarch_asx) + #define SK_CPU_LSX_LEVEL SK_CPU_LSX_LEVEL_LASX + #elif defined(__loongarch_sx) + #define SK_CPU_LSX_LEVEL SK_CPU_LSX_LEVEL_LSX + #endif +#endif + +// Are we in VisualStudio? +#ifndef SK_CPU_SSE_LEVEL + // These checks must be done in descending order to ensure we set the highest + // available SSE level. 64-bit intel guarantees at least SSE2 support. + #if defined(__AVX512F__) && defined(__AVX512DQ__) && defined(__AVX512CD__) && \ + defined(__AVX512BW__) && defined(__AVX512VL__) + #define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SKX + #elif defined(__AVX2__) + #define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_AVX2 + #elif defined(__AVX__) + #define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_AVX + #elif defined(_M_X64) || defined(_M_AMD64) + #define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SSE2 + #elif defined(_M_IX86_FP) + #if _M_IX86_FP >= 2 + #define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SSE2 + #elif _M_IX86_FP == 1 + #define SK_CPU_SSE_LEVEL SK_CPU_SSE_LEVEL_SSE1 + #endif + #endif +#endif + +// ARM defines +#if defined(__arm__) && (!defined(__APPLE__) || !TARGET_IPHONE_SIMULATOR) + #define SK_CPU_ARM32 +#elif defined(__aarch64__) + #define SK_CPU_ARM64 +#endif + +// All 64-bit ARM chips have NEON. Many 32-bit ARM chips do too. +#if !defined(SK_ARM_HAS_NEON) && defined(__ARM_NEON) + #define SK_ARM_HAS_NEON +#endif + +#endif // SkFeatures_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkFixed.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkFixed.h new file mode 100644 index 00000000000..2c8f2fb56c1 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkFixed.h @@ -0,0 +1,143 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFixed_DEFINED +#define SkFixed_DEFINED + +#include "include/private/base/SkAssert.h" +#include "include/private/base/SkDebug.h" +#include "include/private/base/SkMath.h" // IWYU pragma: keep +#include "include/private/base/SkTPin.h" // IWYU pragma: keep + +#include + +/** \file SkFixed.h + + Types and macros for 16.16 fixed point +*/ + +/** 32 bit signed integer used to represent fractions values with 16 bits to the right of the decimal point +*/ +typedef int32_t SkFixed; +#define SK_Fixed1 (1 << 16) +#define SK_FixedHalf (1 << 15) +#define SK_FixedQuarter (1 << 14) +#define SK_FixedMax (0x7FFFFFFF) +#define SK_FixedMin (-SK_FixedMax) +#define SK_FixedPI (0x3243F) +#define SK_FixedSqrt2 (92682) +#define SK_FixedTanPIOver8 (0x6A0A) +#define SK_FixedRoot2Over2 (0xB505) + +// NOTE: SkFixedToFloat is exact. SkFloatToFixed seems to lack a rounding step. For all fixed-point +// values, this version is as accurate as possible for (fixed -> float -> fixed). Rounding reduces +// accuracy if the intermediate floats are in the range that only holds integers (adding 0.5f to an +// odd integer then snaps to nearest even). Using double for the rounding math gives maximum +// accuracy for (float -> fixed -> float), but that's usually overkill. +#define SkFixedToFloat(x) ((x) * 1.52587890625e-5f) +#define SkFloatToFixed(x) sk_float_saturate2int((x) * SK_Fixed1) + +#ifdef SK_DEBUG + static inline SkFixed SkFloatToFixed_Check(float x) { + int64_t n64 = (int64_t)(x * SK_Fixed1); + SkFixed n32 = (SkFixed)n64; + SkASSERT(n64 == n32); + return n32; + } +#else + #define SkFloatToFixed_Check(x) SkFloatToFixed(x) +#endif + +#define SkFixedToDouble(x) ((x) * 1.52587890625e-5) +#define SkDoubleToFixed(x) ((SkFixed)((x) * SK_Fixed1)) + +/** Converts an integer to a SkFixed, asserting that the result does not overflow + a 32 bit signed integer +*/ +#ifdef SK_DEBUG + inline SkFixed SkIntToFixed(int n) + { + SkASSERT(n >= -32768 && n <= 32767); + // Left shifting a negative value has undefined behavior in C, so we cast to unsigned before + // shifting. + return (SkFixed)( (unsigned)n << 16 ); + } +#else + // Left shifting a negative value has undefined behavior in C, so we cast to unsigned before + // shifting. Then we force the cast to SkFixed to ensure that the answer is signed (like the + // debug version). + #define SkIntToFixed(n) (SkFixed)((unsigned)(n) << 16) +#endif + +#define SkFixedRoundToInt(x) (((x) + SK_FixedHalf) >> 16) +#define SkFixedCeilToInt(x) (((x) + SK_Fixed1 - 1) >> 16) +#define SkFixedFloorToInt(x) ((x) >> 16) + +static inline SkFixed SkFixedRoundToFixed(SkFixed x) { + return (SkFixed)( (uint32_t)(x + SK_FixedHalf) & 0xFFFF0000 ); +} +static inline SkFixed SkFixedCeilToFixed(SkFixed x) { + return (SkFixed)( (uint32_t)(x + SK_Fixed1 - 1) & 0xFFFF0000 ); +} +static inline SkFixed SkFixedFloorToFixed(SkFixed x) { + return (SkFixed)( (uint32_t)x & 0xFFFF0000 ); +} + +#define SkFixedAve(a, b) (((a) + (b)) >> 1) + +// The divide may exceed 32 bits. Clamp to a signed 32 bit result. +#define SkFixedDiv(numer, denom) \ + SkToS32(SkTPin((SkLeftShift((int64_t)(numer), 16) / (denom)), SK_MinS32, SK_MaxS32)) + +static inline SkFixed SkFixedMul(SkFixed a, SkFixed b) { + return (SkFixed)((int64_t)a * b >> 16); +} + +/////////////////////////////////////////////////////////////////////////////// +// Platform-specific alternatives to our portable versions. + +// The VCVT float-to-fixed instruction is part of the VFPv3 instruction set. +#if defined(__ARM_VFPV3__) + #include + + /* This does not handle NaN or other obscurities, but is faster than + than (int)(x*65536). When built on Android with -Os, needs forcing + to inline or we lose the speed benefit. + */ + SK_ALWAYS_INLINE SkFixed SkFloatToFixed_arm(float x) + { + int32_t y; + asm("vcvt.s32.f32 %0, %0, #16": "+w"(x)); + std::memcpy(&y, &x, sizeof(y)); + return y; + } + #undef SkFloatToFixed + #define SkFloatToFixed(x) SkFloatToFixed_arm(x) +#endif + +/////////////////////////////////////////////////////////////////////////////// + +#define SkFixedToScalar(x) SkFixedToFloat(x) +#define SkScalarToFixed(x) SkFloatToFixed(x) + +/////////////////////////////////////////////////////////////////////////////// + +typedef int64_t SkFixed3232; // 32.32 + +#define SkFixed3232Max SK_MaxS64 +#define SkFixed3232Min (-SkFixed3232Max) + +#define SkIntToFixed3232(x) (SkLeftShift((SkFixed3232)(x), 32)) +#define SkFixed3232ToInt(x) ((int)((x) >> 32)) +#define SkFixedToFixed3232(x) (SkLeftShift((SkFixed3232)(x), 16)) +#define SkFixed3232ToFixed(x) ((SkFixed)((x) >> 16)) +#define SkFloatToFixed3232(x) sk_float_saturate2int64((x) * (65536.0f * 65536.0f)) +#define SkFixed3232ToFloat(x) (x * (1 / (65536.0f * 65536.0f))) + +#define SkScalarToFixed3232(x) SkFloatToFixed3232(x) + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkFloatingPoint.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkFloatingPoint.h new file mode 100644 index 00000000000..5a1e4e30b7f --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkFloatingPoint.h @@ -0,0 +1,184 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFloatingPoint_DEFINED +#define SkFloatingPoint_DEFINED + +#include "include/private/base/SkAttributes.h" +#include "include/private/base/SkMath.h" + +#include +#include +#include +#include + +inline constexpr float SK_FloatSqrt2 = 1.41421356f; +inline constexpr float SK_FloatPI = 3.14159265f; +inline constexpr double SK_DoublePI = 3.14159265358979323846264338327950288; + +static constexpr int sk_float_sgn(float x) { + return (0.0f < x) - (x < 0.0f); +} + +static constexpr float sk_float_degrees_to_radians(float degrees) { + return degrees * (SK_FloatPI / 180); +} + +static constexpr float sk_float_radians_to_degrees(float radians) { + return radians * (180 / SK_FloatPI); +} + +// floor(double+0.5) vs. floorf(float+0.5f) give comparable performance, but upcasting to double +// means tricky values like 0.49999997 and 2^24 get rounded correctly. If these were rounded +// as floatf(x + .5f), they would be 1 higher than expected. +#define sk_float_round(x) (float)sk_double_round((double)(x)) + +template , bool> = true> +static inline constexpr bool SkIsNaN(T x) { + return x != x; +} + +// Subtracting a value from itself will result in zero, except for NAN or ±Inf, which make NAN. +// Multiplying a group of values against zero will result in zero for each product, except for +// NAN or ±Inf, which will result in NAN and continue resulting in NAN for the rest of the elements. +// This generates better code than `std::isfinite` when building with clang-cl (April 2024). +template , bool> = true> +static inline bool SkIsFinite(T x, Pack... values) { + T prod = x - x; + prod = (prod * ... * values); + // At this point, `prod` will either be NaN or 0. + return prod == prod; +} + +template , bool> = true> +static inline bool SkIsFinite(const T array[], int count) { + T x = array[0]; + T prod = x - x; + for (int i = 1; i < count; ++i) { + prod *= array[i]; + } + // At this point, `prod` will either be NaN or 0. + return prod == prod; +} + +inline constexpr int SK_MaxS32FitsInFloat = 2147483520; +inline constexpr int SK_MinS32FitsInFloat = -SK_MaxS32FitsInFloat; + +// 0x7fffff8000000000 +inline constexpr int64_t SK_MaxS64FitsInFloat = SK_MaxS64 >> (63-24) << (63-24); +inline constexpr int64_t SK_MinS64FitsInFloat = -SK_MaxS64FitsInFloat; + +// sk_[float|double]_saturate2int are written to return their maximum values when passed NaN. +// MSVC 19.38+ has a bug with this implementation, leading to incorrect results: +// https://developercommunity.visualstudio.com/t/Optimizer-incorrectly-handles-NaN-floati/10654403 +// +// We inject an explicit NaN test on MSVC to work around the problem. +#if defined(_MSC_VER) && !defined(__clang__) + #define SK_CHECK_NAN(resultVal) if (SkIsNaN(x)) { return resultVal; } +#else + #define SK_CHECK_NAN(resultVal) +#endif + +/** + * Return the closest int for the given float. Returns SK_MaxS32FitsInFloat for NaN. + */ +static constexpr int sk_float_saturate2int(float x) { + SK_CHECK_NAN(SK_MaxS32FitsInFloat) + x = x < SK_MaxS32FitsInFloat ? x : SK_MaxS32FitsInFloat; + x = x > SK_MinS32FitsInFloat ? x : SK_MinS32FitsInFloat; + return (int)x; +} + +/** + * Return the closest int for the given double. Returns SK_MaxS32 for NaN. + */ +static constexpr int sk_double_saturate2int(double x) { + SK_CHECK_NAN(SK_MaxS32) + x = x < SK_MaxS32 ? x : SK_MaxS32; + x = x > SK_MinS32 ? x : SK_MinS32; + return (int)x; +} + +/** + * Return the closest int64_t for the given float. Returns SK_MaxS64FitsInFloat for NaN. + */ +static constexpr int64_t sk_float_saturate2int64(float x) { + SK_CHECK_NAN(SK_MaxS64FitsInFloat) + x = x < SK_MaxS64FitsInFloat ? x : SK_MaxS64FitsInFloat; + x = x > SK_MinS64FitsInFloat ? x : SK_MinS64FitsInFloat; + return (int64_t)x; +} + +#undef SK_CHECK_NAN + +#define sk_float_floor2int(x) sk_float_saturate2int(std::floor(x)) +#define sk_float_round2int(x) sk_float_saturate2int(sk_float_round(x)) +#define sk_float_ceil2int(x) sk_float_saturate2int(std::ceil(x)) + +#define sk_float_floor2int_no_saturate(x) ((int)std::floor(x)) +#define sk_float_round2int_no_saturate(x) ((int)sk_float_round(x)) +#define sk_float_ceil2int_no_saturate(x) ((int)std::ceil(x)) + +#define sk_double_round(x) (std::floor((x) + 0.5)) +#define sk_double_floor2int(x) ((int)std::floor(x)) +#define sk_double_round2int(x) ((int)std::round(x)) +#define sk_double_ceil2int(x) ((int)std::ceil(x)) + +// Cast double to float, ignoring any warning about too-large finite values being cast to float. +// Clang thinks this is undefined, but it's actually implementation defined to return either +// the largest float or infinity (one of the two bracketing representable floats). Good enough! +SK_NO_SANITIZE("float-cast-overflow") +static constexpr float sk_double_to_float(double x) { + return static_cast(x); +} + +inline constexpr float SK_FloatNaN = std::numeric_limits::quiet_NaN(); +inline constexpr float SK_FloatInfinity = std::numeric_limits::infinity(); +inline constexpr float SK_FloatNegativeInfinity = -SK_FloatInfinity; + +inline constexpr double SK_DoubleNaN = std::numeric_limits::quiet_NaN(); + +// Calculate the midpoint between a and b. Similar to std::midpoint in c++20. +static constexpr float sk_float_midpoint(float a, float b) { + // Use double math to avoid underflow and overflow. + return static_cast(0.5 * (static_cast(a) + b)); +} + +static inline float sk_float_rsqrt_portable(float x) { return 1.0f / std::sqrt(x); } +static inline float sk_float_rsqrt (float x) { return 1.0f / std::sqrt(x); } + +// IEEE defines how float divide behaves for non-finite values and zero-denoms, but C does not, +// so we have a helper that suppresses the possible undefined-behavior warnings. +#ifdef SK_BUILD_FOR_WIN +#pragma warning(push) +#pragma warning(disable : 4723) +#endif +SK_NO_SANITIZE("float-divide-by-zero") +static constexpr float sk_ieee_float_divide(float numer, float denom) { + return numer / denom; +} + +SK_NO_SANITIZE("float-divide-by-zero") +static constexpr double sk_ieee_double_divide(double numer, double denom) { + return numer / denom; +} +#ifdef SK_BUILD_FOR_WIN +#pragma warning( pop ) +#endif + +// Returns true iff the provided number is within a small epsilon of 0. +bool sk_double_nearly_zero(double a); + +// Compare two doubles and return true if they are within maxUlpsDiff of each other. +// * nan as a or b - returns false. +// * infinity, infinity or -infinity, -infinity - returns true. +// * infinity and any other number - returns false. +// +// ulp is an initialism for Units in the Last Place. +bool sk_doubles_nearly_equal_ulps(double a, double b, uint8_t maxUlpsDiff = 16); + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkLoadUserConfig.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkLoadUserConfig.h new file mode 100644 index 00000000000..9f949782c00 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkLoadUserConfig.h @@ -0,0 +1,63 @@ +/* + * Copyright 2022 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SK_USER_CONFIG_WAS_LOADED + +// Include this to set reasonable defaults (e.g. for SK_CPU_LENDIAN) +#include "include/private/base/SkFeatures.h" + +// Allows embedders that want to disable macros that take arguments to just +// define that symbol to be one of these +#define SK_NOTHING_ARG1(arg1) +#define SK_NOTHING_ARG2(arg1, arg2) +#define SK_NOTHING_ARG3(arg1, arg2, arg3) + +// IWYU pragma: begin_exports + +// Note: SK_USER_CONFIG_HEADER will not work with Bazel builds and some C++ compilers. +#if defined(SK_USER_CONFIG_HEADER) + #include SK_USER_CONFIG_HEADER +#elif defined(SK_USE_BAZEL_CONFIG_HEADER) + // The Bazel config file is presumed to be in the root directory of its Bazel Workspace. + // This is achieved in Skia by having a nested WORKSPACE in include/config and a cc_library + // defined in that folder. As a result, we do not try to include SkUserConfig.h from the + // top of Skia because Bazel sandboxing will move it to a different location. + #include "SkUserConfig.h" // NO_G3_REWRITE +#else + #include "include/config/SkUserConfig.h" +#endif +// IWYU pragma: end_exports + +// Checks to make sure the SkUserConfig options do not conflict. +#if !defined(SK_DEBUG) && !defined(SK_RELEASE) + #ifdef NDEBUG + #define SK_RELEASE + #else + #define SK_DEBUG + #endif +#endif + +#if defined(SK_DEBUG) && defined(SK_RELEASE) +# error "cannot define both SK_DEBUG and SK_RELEASE" +#elif !defined(SK_DEBUG) && !defined(SK_RELEASE) +# error "must define either SK_DEBUG or SK_RELEASE" +#endif + +#if defined(SK_CPU_LENDIAN) && defined(SK_CPU_BENDIAN) +# error "cannot define both SK_CPU_LENDIAN and SK_CPU_BENDIAN" +#elif !defined(SK_CPU_LENDIAN) && !defined(SK_CPU_BENDIAN) +# error "must define either SK_CPU_LENDIAN or SK_CPU_BENDIAN" +#endif + +#if defined(SK_CPU_BENDIAN) && !defined(I_ACKNOWLEDGE_SKIA_DOES_NOT_SUPPORT_BIG_ENDIAN) + #error "The Skia team is not endian-savvy enough to support big-endian CPUs." + #error "If you still want to use Skia," + #error "please define I_ACKNOWLEDGE_SKIA_DOES_NOT_SUPPORT_BIG_ENDIAN." +#endif + +#define SK_USER_CONFIG_WAS_LOADED +#endif // SK_USER_CONFIG_WAS_LOADED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkMacros.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkMacros.h new file mode 100644 index 00000000000..5d1835d0130 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkMacros.h @@ -0,0 +1,94 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkMacros_DEFINED +#define SkMacros_DEFINED + +/* + * Usage: SK_MACRO_CONCAT(a, b) to construct the symbol ab + * + * SK_MACRO_CONCAT_IMPL_PRIV just exists to make this work. Do not use directly + * + */ +#define SK_MACRO_CONCAT(X, Y) SK_MACRO_CONCAT_IMPL_PRIV(X, Y) +#define SK_MACRO_CONCAT_IMPL_PRIV(X, Y) X ## Y + +/* + * Usage: SK_MACRO_APPEND_LINE(foo) to make foo123, where 123 is the current + * line number. Easy way to construct + * unique names for local functions or + * variables. + */ +#define SK_MACRO_APPEND_LINE(name) SK_MACRO_CONCAT(name, __LINE__) + +#define SK_MACRO_APPEND_COUNTER(name) SK_MACRO_CONCAT(name, __COUNTER__) + +//////////////////////////////////////////////////////////////////////////////// + +// Can be used to bracket data types that must be dense/packed, e.g. hash keys. +#if defined(__clang__) // This should work on GCC too, but GCC diagnostic pop didn't seem to work! + #define SK_BEGIN_REQUIRE_DENSE _Pragma("GCC diagnostic push") \ + _Pragma("GCC diagnostic error \"-Wpadded\"") + #define SK_END_REQUIRE_DENSE _Pragma("GCC diagnostic pop") +#else + #define SK_BEGIN_REQUIRE_DENSE + #define SK_END_REQUIRE_DENSE +#endif + +#if defined(__clang__) && defined(__has_feature) + // Some compilers have a preprocessor that does not appear to do short-circuit + // evaluation as expected + #if __has_feature(leak_sanitizer) || __has_feature(address_sanitizer) + // Chrome had issues if we tried to include lsan_interface.h ourselves. + // https://github.com/llvm/llvm-project/blob/10a35632d55bb05004fe3d0c2d4432bb74897ee7/compiler-rt/include/sanitizer/lsan_interface.h#L26 +extern "C" { + void __lsan_ignore_object(const void *p); +} + #define SK_INTENTIONALLY_LEAKED(X) __lsan_ignore_object(X) + #else + #define SK_INTENTIONALLY_LEAKED(X) ((void)0) + #endif +#else + #define SK_INTENTIONALLY_LEAKED(X) ((void)0) +#endif + +#define SK_INIT_TO_AVOID_WARNING = 0 + +//////////////////////////////////////////////////////////////////////////////// + +/** + * Defines overloaded bitwise operators to make it easier to use an enum as a + * bitfield. + */ +#define SK_MAKE_BITFIELD_OPS(X) \ + inline X operator ~(X a) { \ + using U = std::underlying_type_t; \ + return (X) (~static_cast(a)); \ + } \ + inline X operator |(X a, X b) { \ + using U = std::underlying_type_t; \ + return (X) (static_cast(a) | static_cast(b)); \ + } \ + inline X& operator |=(X& a, X b) { \ + return (a = a | b); \ + } \ + inline X operator &(X a, X b) { \ + using U = std::underlying_type_t; \ + return (X) (static_cast(a) & static_cast(b)); \ + } \ + inline X& operator &=(X& a, X b) { \ + return (a = a & b); \ + } + +#define SK_DECL_BITFIELD_OPS_FRIENDS(X) \ + friend X operator ~(X a); \ + friend X operator |(X a, X b); \ + friend X& operator |=(X& a, X b); \ + \ + friend X operator &(X a, X b); \ + friend X& operator &=(X& a, X b); + +#endif // SkMacros_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkMalloc.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkMalloc.h new file mode 100644 index 00000000000..60a77ee7a9f --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkMalloc.h @@ -0,0 +1,152 @@ +/* + * Copyright 2017 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkMalloc_DEFINED +#define SkMalloc_DEFINED + +#include + +#include "include/private/base/SkAPI.h" + +/* + memory wrappers to be implemented by the porting layer (platform) +*/ + + +/** Free memory returned by sk_malloc(). It is safe to pass null. */ +SK_API extern void sk_free(void*); + +/** + * Called internally if we run out of memory. The platform implementation must + * not return, but should either throw an exception or otherwise exit. + */ +SK_API extern void sk_out_of_memory(void); + +enum { + /** + * If this bit is set, the returned buffer must be zero-initialized. If this bit is not set + * the buffer can be uninitialized. + */ + SK_MALLOC_ZERO_INITIALIZE = 1 << 0, + + /** + * If this bit is set, the implementation must throw/crash/quit if the request cannot + * be fulfilled. If this bit is not set, then it should return nullptr on failure. + */ + SK_MALLOC_THROW = 1 << 1, +}; +/** + * Return a block of memory (at least 4-byte aligned) of at least the specified size. + * If the requested memory cannot be returned, either return nullptr or throw/exit, depending + * on the SK_MALLOC_THROW bit. If the allocation succeeds, the memory will be zero-initialized + * if the SK_MALLOC_ZERO_INITIALIZE bit was set. + * + * To free the memory, call sk_free() + */ +SK_API extern void* sk_malloc_flags(size_t size, unsigned flags); + +/** Same as standard realloc(), but this one never returns null on failure. It will throw + * if it fails. + * If size is 0, it will call sk_free on buffer and return null. (This behavior is implementation- + * defined for normal realloc. We follow what glibc does.) + */ +SK_API extern void* sk_realloc_throw(void* buffer, size_t size); + +/** + * Return the size of the block of memory allocated in reality for a given pointer. The pointer + * passed must have been allocated using the sk_malloc_* or sk_realloc_* functions. The "size" + * parameter indicates the size originally requested when the memory block was allocated, and + * the value returned by this function must be bigger or equal to it. + */ +SK_API extern size_t sk_malloc_size(void* addr, size_t size); + +static inline void* sk_malloc_throw(size_t size) { + return sk_malloc_flags(size, SK_MALLOC_THROW); +} + +static inline void* sk_calloc_throw(size_t size) { + return sk_malloc_flags(size, SK_MALLOC_THROW | SK_MALLOC_ZERO_INITIALIZE); +} + +static inline void* sk_calloc_canfail(size_t size) { +#if defined(SK_BUILD_FOR_FUZZER) + // To reduce the chance of OOM, pretend we can't allocate more than 200kb. + if (size > 200000) { + return nullptr; + } +#endif + return sk_malloc_flags(size, SK_MALLOC_ZERO_INITIALIZE); +} + +// Performs a safe multiply count * elemSize, checking for overflow +SK_API extern void* sk_calloc_throw(size_t count, size_t elemSize); +SK_API extern void* sk_malloc_throw(size_t count, size_t elemSize); +SK_API extern void* sk_realloc_throw(void* buffer, size_t count, size_t elemSize); + +/** + * These variants return nullptr on failure + */ +static inline void* sk_malloc_canfail(size_t size) { +#if defined(SK_BUILD_FOR_FUZZER) + // To reduce the chance of OOM, pretend we can't allocate more than 200kb. + if (size > 200000) { + return nullptr; + } +#endif + return sk_malloc_flags(size, 0); +} +SK_API extern void* sk_malloc_canfail(size_t count, size_t elemSize); + +// bzero is safer than memset, but we can't rely on it, so... sk_bzero() +static inline void sk_bzero(void* buffer, size_t size) { + // Please c.f. sk_careful_memcpy. It's undefined behavior to call memset(null, 0, 0). + if (size) { + memset(buffer, 0, size); + } +} + +/** + * sk_careful_memcpy() is just like memcpy(), but guards against undefined behavior. + * + * It is undefined behavior to call memcpy() with null dst or src, even if len is 0. + * If an optimizer is "smart" enough, it can exploit this to do unexpected things. + * memcpy(dst, src, 0); + * if (src) { + * printf("%x\n", *src); + * } + * In this code the compiler can assume src is not null and omit the if (src) {...} check, + * unconditionally running the printf, crashing the program if src really is null. + * Of the compilers we pay attention to only GCC performs this optimization in practice. + */ +static inline void* sk_careful_memcpy(void* dst, const void* src, size_t len) { + // When we pass >0 len we had better already be passing valid pointers. + // So we just need to skip calling memcpy when len == 0. + if (len) { + memcpy(dst,src,len); + } + return dst; +} + +static inline void* sk_careful_memmove(void* dst, const void* src, size_t len) { + // When we pass >0 len we had better already be passing valid pointers. + // So we just need to skip calling memcpy when len == 0. + if (len) { + memmove(dst,src,len); + } + return dst; +} + +static inline int sk_careful_memcmp(const void* a, const void* b, size_t len) { + // When we pass >0 len we had better already be passing valid pointers. + // So we just need to skip calling memcmp when len == 0. + if (len == 0) { + return 0; // we treat zero-length buffers as "equal" + } + return memcmp(a, b, len); +} + +#endif // SkMalloc_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkMath.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkMath.h new file mode 100644 index 00000000000..34bfa739f7a --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkMath.h @@ -0,0 +1,77 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkMath_DEFINED +#define SkMath_DEFINED + +#include "include/private/base/SkAssert.h" +#include "include/private/base/SkCPUTypes.h" + +#include +#include + +// Max Signed 16 bit value +static constexpr int16_t SK_MaxS16 = INT16_MAX; +static constexpr int16_t SK_MinS16 = -SK_MaxS16; + +static constexpr int32_t SK_MaxS32 = INT32_MAX; +static constexpr int32_t SK_MinS32 = -SK_MaxS32; +static constexpr int32_t SK_NaN32 = INT32_MIN; + +static constexpr int64_t SK_MaxS64 = INT64_MAX; +static constexpr int64_t SK_MinS64 = -SK_MaxS64; + +// 64bit -> 32bit utilities + +// Handy util that can be passed two ints, and will automatically promote to +// 64bits before the multiply, so the caller doesn't have to remember to cast +// e.g. (int64_t)a * b; +static inline int64_t sk_64_mul(int64_t a, int64_t b) { + return a * b; +} + +static inline constexpr int32_t SkLeftShift(int32_t value, int32_t shift) { + return (int32_t) ((uint32_t) value << shift); +} + +static inline constexpr int64_t SkLeftShift(int64_t value, int32_t shift) { + return (int64_t) ((uint64_t) value << shift); +} + +/////////////////////////////////////////////////////////////////////////////// + +/** + * Returns true if value is a power of 2. Does not explicitly check for + * value <= 0. + */ +template constexpr inline bool SkIsPow2(T value) { + return (value & (value - 1)) == 0; +} + +/////////////////////////////////////////////////////////////////////////////// + +/** + * Return a*b/((1 << shift) - 1), rounding any fractional bits. + * Only valid if a and b are unsigned and <= 32767 and shift is > 0 and <= 8 + */ +static inline unsigned SkMul16ShiftRound(U16CPU a, U16CPU b, int shift) { + SkASSERT(a <= 32767); + SkASSERT(b <= 32767); + SkASSERT(shift > 0 && shift <= 8); + unsigned prod = a*b + (1 << (shift - 1)); + return (prod + (prod >> shift)) >> shift; +} + +/** + * Return a*b/255, rounding any fractional bits. + * Only valid if a and b are unsigned and <= 32767. + */ +static inline U8CPU SkMulDiv255Round(U16CPU a, U16CPU b) { + return SkMul16ShiftRound(a, b, 8); +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkMutex.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkMutex.h new file mode 100644 index 00000000000..4452beb912f --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkMutex.h @@ -0,0 +1,64 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkMutex_DEFINED +#define SkMutex_DEFINED + +#include "include/private/base/SkAssert.h" +#include "include/private/base/SkDebug.h" +#include "include/private/base/SkSemaphore.h" +#include "include/private/base/SkThreadAnnotations.h" +#include "include/private/base/SkThreadID.h" + +class SK_CAPABILITY("mutex") SkMutex { +public: + constexpr SkMutex() = default; + + ~SkMutex() { + this->assertNotHeld(); + } + + void acquire() SK_ACQUIRE() { + fSemaphore.wait(); + SkDEBUGCODE(fOwner = SkGetThreadID();) + } + + void release() SK_RELEASE_CAPABILITY() { + this->assertHeld(); + SkDEBUGCODE(fOwner = kIllegalThreadID;) + fSemaphore.signal(); + } + + void assertHeld() SK_ASSERT_CAPABILITY(this) { + SkASSERT(fOwner == SkGetThreadID()); + } + + void assertNotHeld() { + SkASSERT(fOwner == kIllegalThreadID); + } + +private: + SkSemaphore fSemaphore{1}; + SkDEBUGCODE(SkThreadID fOwner{kIllegalThreadID};) +}; + +class SK_SCOPED_CAPABILITY SkAutoMutexExclusive { +public: + SkAutoMutexExclusive(SkMutex& mutex) SK_ACQUIRE(mutex) : fMutex(mutex) { fMutex.acquire(); } + ~SkAutoMutexExclusive() SK_RELEASE_CAPABILITY() { fMutex.release(); } + + SkAutoMutexExclusive(const SkAutoMutexExclusive&) = delete; + SkAutoMutexExclusive(SkAutoMutexExclusive&&) = delete; + + SkAutoMutexExclusive& operator=(const SkAutoMutexExclusive&) = delete; + SkAutoMutexExclusive& operator=(SkAutoMutexExclusive&&) = delete; + +private: + SkMutex& fMutex; +}; + +#endif // SkMutex_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkNoncopyable.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkNoncopyable.h new file mode 100644 index 00000000000..ec4a4e51611 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkNoncopyable.h @@ -0,0 +1,30 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkNoncopyable_DEFINED +#define SkNoncopyable_DEFINED + +#include "include/private/base/SkAPI.h" + +/** \class SkNoncopyable (DEPRECATED) + + SkNoncopyable is the base class for objects that do not want to + be copied. It hides its copy-constructor and its assignment-operator. +*/ +class SK_API SkNoncopyable { +public: + SkNoncopyable() = default; + + SkNoncopyable(SkNoncopyable&&) = default; + SkNoncopyable& operator =(SkNoncopyable&&) = default; + +private: + SkNoncopyable(const SkNoncopyable&) = delete; + SkNoncopyable& operator=(const SkNoncopyable&) = delete; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkOnce.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkOnce.h new file mode 100644 index 00000000000..97ce6b6311e --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkOnce.h @@ -0,0 +1,55 @@ +/* + * Copyright 2013 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkOnce_DEFINED +#define SkOnce_DEFINED + +#include "include/private/base/SkThreadAnnotations.h" + +#include +#include +#include + +// SkOnce provides call-once guarantees for Skia, much like std::once_flag/std::call_once(). +// +// There should be no particularly error-prone gotcha use cases when using SkOnce. +// It works correctly as a class member, a local, a global, a function-scoped static, whatever. + +class SkOnce { +public: + constexpr SkOnce() = default; + + template + void operator()(Fn&& fn, Args&&... args) { + auto state = fState.load(std::memory_order_acquire); + + if (state == Done) { + return; + } + + // If it looks like no one has started calling fn(), try to claim that job. + if (state == NotStarted && fState.compare_exchange_strong(state, Claimed, + std::memory_order_relaxed, + std::memory_order_relaxed)) { + // Great! We'll run fn() then notify the other threads by releasing Done into fState. + fn(std::forward(args)...); + return fState.store(Done, std::memory_order_release); + } + + // Some other thread is calling fn(). + // We'll just spin here acquiring until it releases Done into fState. + SK_POTENTIALLY_BLOCKING_REGION_BEGIN; + while (fState.load(std::memory_order_acquire) != Done) { /*spin*/ } + SK_POTENTIALLY_BLOCKING_REGION_END; + } + +private: + enum State : uint8_t { NotStarted, Claimed, Done}; + std::atomic fState{NotStarted}; +}; + +#endif // SkOnce_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkPoint_impl.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkPoint_impl.h new file mode 100644 index 00000000000..e3843b240b2 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkPoint_impl.h @@ -0,0 +1,560 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPoint_DEFINED +#define SkPoint_DEFINED + +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkFloatingPoint.h" +#include "include/private/base/SkSafe32.h" + +#include +#include + +struct SkIPoint; + +/** SkIVector provides an alternative name for SkIPoint. SkIVector and SkIPoint + can be used interchangeably for all purposes. +*/ +typedef SkIPoint SkIVector; + +/** \struct SkIPoint + SkIPoint holds two 32-bit integer coordinates. +*/ +struct SkIPoint { + int32_t fX; //!< x-axis value + int32_t fY; //!< y-axis value + + /** Sets fX to x, fY to y. + + @param x integer x-axis value of constructed SkIPoint + @param y integer y-axis value of constructed SkIPoint + @return SkIPoint (x, y) + */ + static constexpr SkIPoint Make(int32_t x, int32_t y) { + return {x, y}; + } + + /** Returns x-axis value of SkIPoint. + + @return fX + */ + constexpr int32_t x() const { return fX; } + + /** Returns y-axis value of SkIPoint. + + @return fY + */ + constexpr int32_t y() const { return fY; } + + /** Returns true if fX and fY are both zero. + + @return true if fX is zero and fY is zero + */ + bool isZero() const { return (fX | fY) == 0; } + + /** Sets fX to x and fY to y. + + @param x new value for fX + @param y new value for fY + */ + void set(int32_t x, int32_t y) { + fX = x; + fY = y; + } + + /** Returns SkIPoint changing the signs of fX and fY. + + @return SkIPoint as (-fX, -fY) + */ + SkIPoint operator-() const { + return {-fX, -fY}; + } + + /** Offsets SkIPoint by ivector v. Sets SkIPoint to (fX + v.fX, fY + v.fY). + + @param v ivector to add + */ + void operator+=(const SkIVector& v) { + fX = Sk32_sat_add(fX, v.fX); + fY = Sk32_sat_add(fY, v.fY); + } + + /** Subtracts ivector v from SkIPoint. Sets SkIPoint to: (fX - v.fX, fY - v.fY). + + @param v ivector to subtract + */ + void operator-=(const SkIVector& v) { + fX = Sk32_sat_sub(fX, v.fX); + fY = Sk32_sat_sub(fY, v.fY); + } + + /** Returns true if SkIPoint is equivalent to SkIPoint constructed from (x, y). + + @param x value compared with fX + @param y value compared with fY + @return true if SkIPoint equals (x, y) + */ + bool equals(int32_t x, int32_t y) const { + return fX == x && fY == y; + } + + /** Returns true if a is equivalent to b. + + @param a SkIPoint to compare + @param b SkIPoint to compare + @return true if a.fX == b.fX and a.fY == b.fY + */ + friend bool operator==(const SkIPoint& a, const SkIPoint& b) { + return a.fX == b.fX && a.fY == b.fY; + } + + /** Returns true if a is not equivalent to b. + + @param a SkIPoint to compare + @param b SkIPoint to compare + @return true if a.fX != b.fX or a.fY != b.fY + */ + friend bool operator!=(const SkIPoint& a, const SkIPoint& b) { + return a.fX != b.fX || a.fY != b.fY; + } + + /** Returns ivector from b to a; computed as (a.fX - b.fX, a.fY - b.fY). + + Can also be used to subtract ivector from ivector, returning ivector. + + @param a SkIPoint or ivector to subtract from + @param b ivector to subtract + @return ivector from b to a + */ + friend SkIVector operator-(const SkIPoint& a, const SkIPoint& b) { + return { Sk32_sat_sub(a.fX, b.fX), Sk32_sat_sub(a.fY, b.fY) }; + } + + /** Returns SkIPoint resulting from SkIPoint a offset by ivector b, computed as: + (a.fX + b.fX, a.fY + b.fY). + + Can also be used to offset SkIPoint b by ivector a, returning SkIPoint. + Can also be used to add ivector to ivector, returning ivector. + + @param a SkIPoint or ivector to add to + @param b SkIPoint or ivector to add + @return SkIPoint equal to a offset by b + */ + friend SkIPoint operator+(const SkIPoint& a, const SkIVector& b) { + return { Sk32_sat_add(a.fX, b.fX), Sk32_sat_add(a.fY, b.fY) }; + } +}; + +struct SkPoint; + +/** SkVector provides an alternative name for SkPoint. SkVector and SkPoint can + be used interchangeably for all purposes. +*/ +typedef SkPoint SkVector; + +/** \struct SkPoint + SkPoint holds two 32-bit floating point coordinates. +*/ +struct SK_API SkPoint { + float fX; //!< x-axis value + float fY; //!< y-axis value + + /** Sets fX to x, fY to y. Used both to set SkPoint and vector. + + @param x float x-axis value of constructed SkPoint or vector + @param y float y-axis value of constructed SkPoint or vector + @return SkPoint (x, y) + */ + static constexpr SkPoint Make(float x, float y) { + return {x, y}; + } + + /** Returns x-axis value of SkPoint or vector. + + @return fX + */ + constexpr float x() const { return fX; } + + /** Returns y-axis value of SkPoint or vector. + + @return fY + */ + constexpr float y() const { return fY; } + + /** Returns true if fX and fY are both zero. + + @return true if fX is zero and fY is zero + */ + bool isZero() const { return (0 == fX) & (0 == fY); } + + /** Sets fX to x and fY to y. + + @param x new value for fX + @param y new value for fY + */ + void set(float x, float y) { + fX = x; + fY = y; + } + + /** Sets fX to x and fY to y, promoting integers to float values. + + Assigning a large integer value directly to fX or fY may cause a compiler + error, triggered by narrowing conversion of int to float. This safely + casts x and y to avoid the error. + + @param x new value for fX + @param y new value for fY + */ + void iset(int32_t x, int32_t y) { + fX = static_cast(x); + fY = static_cast(y); + } + + /** Sets fX to p.fX and fY to p.fY, promoting integers to float values. + + Assigning an SkIPoint containing a large integer value directly to fX or fY may + cause a compiler error, triggered by narrowing conversion of int to float. + This safely casts p.fX and p.fY to avoid the error. + + @param p SkIPoint members promoted to float + */ + void iset(const SkIPoint& p) { + fX = static_cast(p.fX); + fY = static_cast(p.fY); + } + + /** Sets fX to absolute value of pt.fX; and fY to absolute value of pt.fY. + + @param pt members providing magnitude for fX and fY + */ + void setAbs(const SkPoint& pt) { + fX = std::abs(pt.fX); + fY = std::abs(pt.fY); + } + + /** Adds offset to each SkPoint in points array with count entries. + + @param points SkPoint array + @param count entries in array + @param offset vector added to points + */ + static void Offset(SkPoint points[], int count, const SkVector& offset) { + Offset(points, count, offset.fX, offset.fY); + } + + /** Adds offset (dx, dy) to each SkPoint in points array of length count. + + @param points SkPoint array + @param count entries in array + @param dx added to fX in points + @param dy added to fY in points + */ + static void Offset(SkPoint points[], int count, float dx, float dy) { + for (int i = 0; i < count; ++i) { + points[i].offset(dx, dy); + } + } + + /** Adds offset (dx, dy) to SkPoint. + + @param dx added to fX + @param dy added to fY + */ + void offset(float dx, float dy) { + fX += dx; + fY += dy; + } + + /** Returns the Euclidean distance from origin, computed as: + + sqrt(fX * fX + fY * fY) + + . + + @return straight-line distance to origin + */ + float length() const { return SkPoint::Length(fX, fY); } + + /** Returns the Euclidean distance from origin, computed as: + + sqrt(fX * fX + fY * fY) + + . + + @return straight-line distance to origin + */ + float distanceToOrigin() const { return this->length(); } + + /** Scales (fX, fY) so that length() returns one, while preserving ratio of fX to fY, + if possible. If prior length is nearly zero, sets vector to (0, 0) and returns + false; otherwise returns true. + + @return true if former length is not zero or nearly zero + + example: https://fiddle.skia.org/c/@Point_normalize_2 + */ + bool normalize(); + + /** Sets vector to (x, y) scaled so length() returns one, and so that + (fX, fY) is proportional to (x, y). If (x, y) length is nearly zero, + sets vector to (0, 0) and returns false; otherwise returns true. + + @param x proportional value for fX + @param y proportional value for fY + @return true if (x, y) length is not zero or nearly zero + + example: https://fiddle.skia.org/c/@Point_setNormalize + */ + bool setNormalize(float x, float y); + + /** Scales vector so that distanceToOrigin() returns length, if possible. If former + length is nearly zero, sets vector to (0, 0) and return false; otherwise returns + true. + + @param length straight-line distance to origin + @return true if former length is not zero or nearly zero + + example: https://fiddle.skia.org/c/@Point_setLength + */ + bool setLength(float length); + + /** Sets vector to (x, y) scaled to length, if possible. If former + length is nearly zero, sets vector to (0, 0) and return false; otherwise returns + true. + + @param x proportional value for fX + @param y proportional value for fY + @param length straight-line distance to origin + @return true if (x, y) length is not zero or nearly zero + + example: https://fiddle.skia.org/c/@Point_setLength_2 + */ + bool setLength(float x, float y, float length); + + /** Sets dst to SkPoint times scale. dst may be SkPoint to modify SkPoint in place. + + @param scale factor to multiply SkPoint by + @param dst storage for scaled SkPoint + + example: https://fiddle.skia.org/c/@Point_scale + */ + void scale(float scale, SkPoint* dst) const; + + /** Scales SkPoint in place by scale. + + @param value factor to multiply SkPoint by + */ + void scale(float value) { this->scale(value, this); } + + /** Changes the sign of fX and fY. + */ + void negate() { + fX = -fX; + fY = -fY; + } + + /** Returns SkPoint changing the signs of fX and fY. + + @return SkPoint as (-fX, -fY) + */ + SkPoint operator-() const { + return {-fX, -fY}; + } + + /** Adds vector v to SkPoint. Sets SkPoint to: (fX + v.fX, fY + v.fY). + + @param v vector to add + */ + void operator+=(const SkVector& v) { + fX += v.fX; + fY += v.fY; + } + + /** Subtracts vector v from SkPoint. Sets SkPoint to: (fX - v.fX, fY - v.fY). + + @param v vector to subtract + */ + void operator-=(const SkVector& v) { + fX -= v.fX; + fY -= v.fY; + } + + /** Returns SkPoint multiplied by scale. + + @param scale float to multiply by + @return SkPoint as (fX * scale, fY * scale) + */ + SkPoint operator*(float scale) const { + return {fX * scale, fY * scale}; + } + + /** Multiplies SkPoint by scale. Sets SkPoint to: (fX * scale, fY * scale). + + @param scale float to multiply by + @return reference to SkPoint + */ + SkPoint& operator*=(float scale) { + fX *= scale; + fY *= scale; + return *this; + } + + /** Returns true if both fX and fY are measurable values. + + @return true for values other than infinities and NaN + */ + bool isFinite() const { + return SkIsFinite(fX, fY); + } + + /** Returns true if SkPoint is equivalent to SkPoint constructed from (x, y). + + @param x value compared with fX + @param y value compared with fY + @return true if SkPoint equals (x, y) + */ + bool equals(float x, float y) const { + return fX == x && fY == y; + } + + /** Returns true if a is equivalent to b. + + @param a SkPoint to compare + @param b SkPoint to compare + @return true if a.fX == b.fX and a.fY == b.fY + */ + friend bool operator==(const SkPoint& a, const SkPoint& b) { + return a.fX == b.fX && a.fY == b.fY; + } + + /** Returns true if a is not equivalent to b. + + @param a SkPoint to compare + @param b SkPoint to compare + @return true if a.fX != b.fX or a.fY != b.fY + */ + friend bool operator!=(const SkPoint& a, const SkPoint& b) { + return a.fX != b.fX || a.fY != b.fY; + } + + /** Returns vector from b to a, computed as (a.fX - b.fX, a.fY - b.fY). + + Can also be used to subtract vector from SkPoint, returning SkPoint. + Can also be used to subtract vector from vector, returning vector. + + @param a SkPoint to subtract from + @param b SkPoint to subtract + @return vector from b to a + */ + friend SkVector operator-(const SkPoint& a, const SkPoint& b) { + return {a.fX - b.fX, a.fY - b.fY}; + } + + /** Returns SkPoint resulting from SkPoint a offset by vector b, computed as: + (a.fX + b.fX, a.fY + b.fY). + + Can also be used to offset SkPoint b by vector a, returning SkPoint. + Can also be used to add vector to vector, returning vector. + + @param a SkPoint or vector to add to + @param b SkPoint or vector to add + @return SkPoint equal to a offset by b + */ + friend SkPoint operator+(const SkPoint& a, const SkVector& b) { + return {a.fX + b.fX, a.fY + b.fY}; + } + + /** Returns the Euclidean distance from origin, computed as: + + sqrt(x * x + y * y) + + . + + @param x component of length + @param y component of length + @return straight-line distance to origin + + example: https://fiddle.skia.org/c/@Point_Length + */ + static float Length(float x, float y); + + /** Scales (vec->fX, vec->fY) so that length() returns one, while preserving ratio of vec->fX + to vec->fY, if possible. If original length is nearly zero, sets vec to (0, 0) and returns + zero; otherwise, returns length of vec before vec is scaled. + + Returned prior length may be INFINITY if it can not be represented by float. + + Note that normalize() is faster if prior length is not required. + + @param vec normalized to unit length + @return original vec length + + example: https://fiddle.skia.org/c/@Point_Normalize + */ + static float Normalize(SkVector* vec); + + /** Returns the Euclidean distance between a and b. + + @param a line end point + @param b line end point + @return straight-line distance from a to b + */ + static float Distance(const SkPoint& a, const SkPoint& b) { + return Length(a.fX - b.fX, a.fY - b.fY); + } + + /** Returns the dot product of vector a and vector b. + + @param a left side of dot product + @param b right side of dot product + @return product of input magnitudes and cosine of the angle between them + */ + static float DotProduct(const SkVector& a, const SkVector& b) { + return a.fX * b.fX + a.fY * b.fY; + } + + /** Returns the cross product of vector a and vector b. + + a and b form three-dimensional vectors with z-axis value equal to zero. The + cross product is a three-dimensional vector with x-axis and y-axis values equal + to zero. The cross product z-axis component is returned. + + @param a left side of cross product + @param b right side of cross product + @return area spanned by vectors signed by angle direction + */ + static float CrossProduct(const SkVector& a, const SkVector& b) { + return a.fX * b.fY - a.fY * b.fX; + } + + /** Returns the cross product of vector and vec. + + Vector and vec form three-dimensional vectors with z-axis value equal to zero. + The cross product is a three-dimensional vector with x-axis and y-axis values + equal to zero. The cross product z-axis component is returned. + + @param vec right side of cross product + @return area spanned by vectors signed by angle direction + */ + float cross(const SkVector& vec) const { + return CrossProduct(*this, vec); + } + + /** Returns the dot product of vector and vector vec. + + @param vec right side of dot product + @return product of input magnitudes and cosine of the angle between them + */ + float dot(const SkVector& vec) const { + return DotProduct(*this, vec); + } + +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkSafe32.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkSafe32.h new file mode 100644 index 00000000000..5ba4c2f9a48 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkSafe32.h @@ -0,0 +1,49 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkSafe32_DEFINED +#define SkSafe32_DEFINED + +#include "include/private/base/SkAssert.h" +#include "include/private/base/SkMath.h" + +#include + +static constexpr int32_t Sk64_pin_to_s32(int64_t x) { + return x < SK_MinS32 ? SK_MinS32 : (x > SK_MaxS32 ? SK_MaxS32 : (int32_t)x); +} + +static constexpr int32_t Sk32_sat_add(int32_t a, int32_t b) { + return Sk64_pin_to_s32((int64_t)a + (int64_t)b); +} + +static constexpr int32_t Sk32_sat_sub(int32_t a, int32_t b) { + return Sk64_pin_to_s32((int64_t)a - (int64_t)b); +} + +// To avoid UBSAN complaints about 2's compliment overflows +// +static constexpr int32_t Sk32_can_overflow_add(int32_t a, int32_t b) { + return (int32_t)((uint32_t)a + (uint32_t)b); +} +static constexpr int32_t Sk32_can_overflow_sub(int32_t a, int32_t b) { + return (int32_t)((uint32_t)a - (uint32_t)b); +} + +/** + * This is a 'safe' abs for 32-bit integers that asserts when undefined behavior would occur. + * SkTAbs (in SkTemplates.h) is a general purpose absolute-value function. + */ +static inline int32_t SkAbs32(int32_t value) { + SkASSERT(value != SK_NaN32); // The most negative int32_t can't be negated. + if (value < 0) { + value = -value; + } + return value; +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkSemaphore.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkSemaphore.h new file mode 100644 index 00000000000..f78ee86625a --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkSemaphore.h @@ -0,0 +1,84 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkSemaphore_DEFINED +#define SkSemaphore_DEFINED + +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkOnce.h" +#include "include/private/base/SkThreadAnnotations.h" + +#include +#include + +class SkSemaphore { +public: + constexpr SkSemaphore(int count = 0) : fCount(count), fOSSemaphore(nullptr) {} + + // Cleanup the underlying OS semaphore. + SK_SPI ~SkSemaphore(); + + // Increment the counter n times. + // Generally it's better to call signal(n) instead of signal() n times. + void signal(int n = 1); + + // Decrement the counter by 1, + // then if the counter is < 0, sleep this thread until the counter is >= 0. + void wait(); + + // If the counter is positive, decrement it by 1 and return true, otherwise return false. + SK_SPI bool try_wait(); + +private: + // This implementation follows the general strategy of + // 'A Lightweight Semaphore with Partial Spinning' + // found here + // http://preshing.com/20150316/semaphores-are-surprisingly-versatile/ + // That article (and entire blog) are very much worth reading. + // + // We wrap an OS-provided semaphore with a user-space atomic counter that + // lets us avoid interacting with the OS semaphore unless strictly required: + // moving the count from >=0 to <0 or vice-versa, i.e. sleeping or waking threads. + struct OSSemaphore; + + SK_SPI void osSignal(int n); + SK_SPI void osWait(); + + std::atomic fCount; + SkOnce fOSSemaphoreOnce; + OSSemaphore* fOSSemaphore; +}; + +inline void SkSemaphore::signal(int n) { + int prev = fCount.fetch_add(n, std::memory_order_release); + + // We only want to call the OS semaphore when our logical count crosses + // from <0 to >=0 (when we need to wake sleeping threads). + // + // This is easiest to think about with specific examples of prev and n. + // If n == 5 and prev == -3, there are 3 threads sleeping and we signal + // std::min(-(-3), 5) == 3 times on the OS semaphore, leaving the count at 2. + // + // If prev >= 0, no threads are waiting, std::min(-prev, n) is always <= 0, + // so we don't call the OS semaphore, leaving the count at (prev + n). + int toSignal = std::min(-prev, n); + if (toSignal > 0) { + this->osSignal(toSignal); + } +} + +inline void SkSemaphore::wait() { + // Since this fetches the value before the subtract, zero and below means that there are no + // resources left, so the thread needs to wait. + if (fCount.fetch_sub(1, std::memory_order_acquire) <= 0) { + SK_POTENTIALLY_BLOCKING_REGION_BEGIN; + this->osWait(); + SK_POTENTIALLY_BLOCKING_REGION_END; + } +} + +#endif//SkSemaphore_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkSpan_impl.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkSpan_impl.h new file mode 100644 index 00000000000..09b2a754e9f --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkSpan_impl.h @@ -0,0 +1,131 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkSpan_DEFINED +#define SkSpan_DEFINED + +#include "include/private/base/SkAssert.h" +#include "include/private/base/SkDebug.h" +#include "include/private/base/SkTo.h" + +#include +#include +#include +#include +#include + +// Having this be an export works around IWYU churn related to +// https://github.com/include-what-you-use/include-what-you-use/issues/1121 +#include // IWYU pragma: export + +// Add macro to check the lifetime of initializer_list arguments. initializer_list has a very +// short life span, and can only be used as a parameter, and not as a variable. +#if defined(__clang__) && defined(__has_cpp_attribute) && __has_cpp_attribute(clang::lifetimebound) +#define SK_CHECK_IL_LIFETIME [[clang::lifetimebound]] +#else +#define SK_CHECK_IL_LIFETIME +#endif + +/** + * SkSpan holds a reference to contiguous data of type T along with a count. SkSpan does not own + * the data itself but is merely a reference, therefore you must take care with the lifetime of + * the underlying data. + * + * SkSpan is a count and a pointer into existing array or data type that stores its data in + * contiguous memory like std::vector. Any container that works with std::size() and std::data() + * can be used. + * + * SkSpan makes a convenient parameter for a routine to accept array like things. This allows you to + * write the routine without overloads for all different container types. + * + * Example: + * void routine(SkSpan a) { ... } + * + * std::vector v = {1, 2, 3, 4, 5}; + * + * routine(a); + * + * A word of caution when working with initializer_list, initializer_lists have a lifetime that is + * limited to the current statement. The following is correct and safe: + * + * Example: + * routine({1,2,3,4,5}); + * + * The following is undefined, and will result in erratic execution: + * + * Bad Example: + * initializer_list l = {1, 2, 3, 4, 5}; // The data behind l dies at the ;. + * routine(l); + */ +template +class SkSpan { +public: + constexpr SkSpan() : fPtr{nullptr}, fSize{0} {} + + template , bool> = true> + constexpr SkSpan(T* ptr, Integer size) : fPtr{ptr}, fSize{SkToSizeT(size)} { + SkASSERT(ptr || fSize == 0); // disallow nullptr + a nonzero size + SkASSERT(fSize < kMaxSize); + } + template >> + constexpr SkSpan(const SkSpan& that) : fPtr(std::data(that)), fSize(std::size(that)) {} + constexpr SkSpan(const SkSpan& o) = default; + template constexpr SkSpan(T(&a)[N]) : SkSpan(a, N) { } + template + constexpr SkSpan(Container&& c) : SkSpan(std::data(c), std::size(c)) { } + SkSpan(std::initializer_list il SK_CHECK_IL_LIFETIME) + : SkSpan(std::data(il), std::size(il)) {} + + constexpr SkSpan& operator=(const SkSpan& that) = default; + + constexpr T& operator [] (size_t i) const { + return fPtr[sk_collection_check_bounds(i, this->size())]; + } + constexpr T& front() const { sk_collection_not_empty(this->empty()); return fPtr[0]; } + constexpr T& back() const { sk_collection_not_empty(this->empty()); return fPtr[fSize - 1]; } + constexpr T* begin() const { return fPtr; } + constexpr T* end() const { return fPtr + fSize; } + constexpr auto rbegin() const { return std::make_reverse_iterator(this->end()); } + constexpr auto rend() const { return std::make_reverse_iterator(this->begin()); } + constexpr T* data() const { return this->begin(); } + constexpr size_t size() const { return fSize; } + constexpr bool empty() const { return fSize == 0; } + constexpr size_t size_bytes() const { return fSize * sizeof(T); } + constexpr SkSpan first(size_t prefixLen) const { + return SkSpan{fPtr, sk_collection_check_length(prefixLen, fSize)}; + } + constexpr SkSpan last(size_t postfixLen) const { + return SkSpan{fPtr + (this->size() - postfixLen), + sk_collection_check_length(postfixLen, fSize)}; + } + constexpr SkSpan subspan(size_t offset) const { + return this->subspan(offset, this->size() - offset); + } + constexpr SkSpan subspan(size_t offset, size_t count) const { + const size_t safeOffset = sk_collection_check_length(offset, fSize); + + // Should read offset + count > size(), but that could overflow. We know that safeOffset + // is <= size, therefore the subtraction will not overflow. + if (count > this->size() - safeOffset) SK_UNLIKELY { + // The count is too large. + SkUNREACHABLE; + } + return SkSpan{fPtr + safeOffset, count}; + } + +private: + static constexpr size_t kMaxSize = std::numeric_limits::max() / sizeof(T); + + T* fPtr; + size_t fSize; +}; + +template +SkSpan(Container&&) -> + SkSpan()))>>; + +#endif // SkSpan_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTArray.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTArray.h new file mode 100644 index 00000000000..bb17c854c3a --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTArray.h @@ -0,0 +1,806 @@ +/* + * Copyright 2011 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTArray_DEFINED +#define SkTArray_DEFINED + +#include "include/private/base/SkASAN.h" // IWYU pragma: keep +#include "include/private/base/SkAlignedStorage.h" +#include "include/private/base/SkAssert.h" +#include "include/private/base/SkAttributes.h" +#include "include/private/base/SkContainers.h" +#include "include/private/base/SkDebug.h" +#include "include/private/base/SkMalloc.h" +#include "include/private/base/SkMath.h" +#include "include/private/base/SkSpan_impl.h" +#include "include/private/base/SkTo.h" +#include "include/private/base/SkTypeTraits.h" // IWYU pragma: keep + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace skia_private { +/** TArray implements a typical, mostly std::vector-like array. + Each T will be default-initialized on allocation, and ~T will be called on destruction. + + MEM_MOVE controls the behavior when a T needs to be moved (e.g. when the array is resized) + - true: T will be bit-copied via memcpy. + - false: T will be moved via move-constructors. +*/ +template > class TArray { +public: + using value_type = T; + + /** + * Creates an empty array with no initial storage + */ + TArray() : fOwnMemory(true), fCapacity{0} {} + + /** + * Creates an empty array that will preallocate space for reserveCount elements. + */ + explicit TArray(int reserveCount) : TArray() { this->reserve_exact(reserveCount); } + + /** + * Copies one array to another. The new array will be heap allocated. + */ + TArray(const TArray& that) : TArray(that.fData, that.fSize) {} + + TArray(TArray&& that) { + if (that.fOwnMemory) { + this->setData(that); + that.setData({}); + } else { + this->initData(that.fSize); + that.move(fData); + } + this->changeSize(that.fSize); + that.changeSize(0); + } + + /** + * Creates a TArray by copying contents of a standard C array. The new + * array will be heap allocated. Be careful not to use this constructor + * when you really want the (void*, int) version. + */ + TArray(const T* array, int count) { + this->initData(count); + this->copy(array); + } + + /** + * Creates a TArray by copying contents of an initializer list. + */ + TArray(std::initializer_list data) : TArray(data.begin(), data.size()) {} + + TArray& operator=(const TArray& that) { + if (this == &that) { + return *this; + } + this->clear(); + this->checkRealloc(that.size(), kExactFit); + this->changeSize(that.fSize); + this->copy(that.fData); + return *this; + } + + TArray& operator=(TArray&& that) { + if (this != &that) { + this->clear(); + this->unpoison(); + that.unpoison(); + if (that.fOwnMemory) { + // The storage is on the heap, so move the data pointer. + if (fOwnMemory) { + sk_free(fData); + } + + fData = std::exchange(that.fData, nullptr); + + // Can't use exchange with bitfields. + fCapacity = that.fCapacity; + that.fCapacity = 0; + + fOwnMemory = true; + + this->changeSize(that.fSize); + } else { + // The data is stored inline in that, so move it element-by-element. + this->checkRealloc(that.size(), kExactFit); + this->changeSize(that.fSize); + that.move(fData); + } + that.changeSize(0); + } + return *this; + } + + ~TArray() { + this->destroyAll(); + this->unpoison(); + if (fOwnMemory) { + sk_free(fData); + } + } + + /** + * Resets to size() = n newly constructed T objects and resets any reserve count. + */ + void reset(int n) { + SkASSERT(n >= 0); + this->clear(); + this->checkRealloc(n, kExactFit); + this->changeSize(n); + for (int i = 0; i < this->size(); ++i) { + new (fData + i) T; + } + } + + /** + * Resets to a copy of a C array and resets any reserve count. + */ + void reset(const T* array, int count) { + SkASSERT(count >= 0); + this->clear(); + this->checkRealloc(count, kExactFit); + this->changeSize(count); + this->copy(array); + } + + /** + * Ensures there is enough reserved space for at least n elements. This is guaranteed at least + * until the array size grows above n and subsequently shrinks below n, any version of reset() + * is called, or reserve() is called again. + */ + void reserve(int n) { + SkASSERT(n >= 0); + if (n > this->size()) { + this->checkRealloc(n - this->size(), kGrowing); + } + } + + /** + * Ensures there is enough reserved space for exactly n elements. The same capacity guarantees + * as above apply. + */ + void reserve_exact(int n) { + SkASSERT(n >= 0); + if (n > this->size()) { + this->checkRealloc(n - this->size(), kExactFit); + } + } + + void removeShuffle(int n) { + SkASSERT(n < this->size()); + int newCount = fSize - 1; + fData[n].~T(); + if (n != newCount) { + this->move(n, newCount); + } + this->changeSize(newCount); + } + + // Is the array empty. + bool empty() const { return fSize == 0; } + + /** + * Adds one new default-initialized T value and returns it by reference. Note that the reference + * only remains valid until the next call that adds or removes elements. + */ + T& push_back() { + void* newT = this->push_back_raw(1); + return *new (newT) T; + } + + /** + * Adds one new T value which is copy-constructed, returning it by reference. As always, + * the reference only remains valid until the next call that adds or removes elements. + */ + T& push_back(const T& t) { + this->unpoison(); + T* newT; + if (this->capacity() > fSize) SK_LIKELY { + // Copy over the element directly. + newT = new (fData + fSize) T(t); + } else { + newT = this->growAndConstructAtEnd(t); + } + + this->changeSize(fSize + 1); + return *newT; + } + + /** + * Adds one new T value which is copy-constructed, returning it by reference. + */ + T& push_back(T&& t) { + this->unpoison(); + T* newT; + if (this->capacity() > fSize) SK_LIKELY { + // Move over the element directly. + newT = new (fData + fSize) T(std::move(t)); + } else { + newT = this->growAndConstructAtEnd(std::move(t)); + } + + this->changeSize(fSize + 1); + return *newT; + } + + /** + * Constructs a new T at the back of this array, returning it by reference. + */ + template T& emplace_back(Args&&... args) { + this->unpoison(); + T* newT; + if (this->capacity() > fSize) SK_LIKELY { + // Emplace the new element in directly. + newT = new (fData + fSize) T(std::forward(args)...); + } else { + newT = this->growAndConstructAtEnd(std::forward(args)...); + } + + this->changeSize(fSize + 1); + return *newT; + } + + /** + * Allocates n more default-initialized T values, and returns the address of + * the start of that new range. Note: this address is only valid until the + * next API call made on the array that might add or remove elements. + */ + T* push_back_n(int n) { + SkASSERT(n >= 0); + T* newTs = TCast(this->push_back_raw(n)); + for (int i = 0; i < n; ++i) { + new (&newTs[i]) T; + } + return newTs; + } + + /** + * Version of above that uses a copy constructor to initialize all n items + * to the same T. + */ + T* push_back_n(int n, const T& t) { + SkASSERT(n >= 0); + T* newTs = TCast(this->push_back_raw(n)); + for (int i = 0; i < n; ++i) { + new (&newTs[i]) T(t); + } + return static_cast(newTs); + } + + /** + * Version of above that uses a copy constructor to initialize the n items + * to separate T values. + */ + T* push_back_n(int n, const T t[]) { + SkASSERT(n >= 0); + this->checkRealloc(n, kGrowing); + T* end = this->end(); + this->changeSize(fSize + n); + for (int i = 0; i < n; ++i) { + new (end + i) T(t[i]); + } + return end; + } + + /** + * Version of above that uses the move constructor to set n items. + */ + T* move_back_n(int n, T* t) { + SkASSERT(n >= 0); + this->checkRealloc(n, kGrowing); + T* end = this->end(); + this->changeSize(fSize + n); + for (int i = 0; i < n; ++i) { + new (end + i) T(std::move(t[i])); + } + return end; + } + + /** + * Removes the last element. Not safe to call when size() == 0. + */ + void pop_back() { + sk_collection_not_empty(this->empty()); + fData[fSize - 1].~T(); + this->changeSize(fSize - 1); + } + + /** + * Removes the last n elements. Not safe to call when size() < n. + */ + void pop_back_n(int n) { + SkASSERT(n >= 0); + SkASSERT(this->size() >= n); + int i = fSize; + while (i-- > fSize - n) { + (*this)[i].~T(); + } + this->changeSize(fSize - n); + } + + /** + * Pushes or pops from the back to resize. Pushes will be default initialized. + */ + void resize_back(int newCount) { + SkASSERT(newCount >= 0); + if (newCount > this->size()) { + if (this->empty()) { + // When the container is completely empty, grow to exactly the requested size. + this->checkRealloc(newCount, kExactFit); + } + this->push_back_n(newCount - fSize); + } else if (newCount < this->size()) { + this->pop_back_n(fSize - newCount); + } + } + + /** Swaps the contents of this array with that array. Does a pointer swap if possible, + otherwise copies the T values. */ + void swap(TArray& that) { + using std::swap; + if (this == &that) { + return; + } + if (fOwnMemory && that.fOwnMemory) { + swap(fData, that.fData); + swap(fSize, that.fSize); + + // Can't use swap because fCapacity is a bit field. + auto allocCount = fCapacity; + fCapacity = that.fCapacity; + that.fCapacity = allocCount; + } else { + // This could be more optimal... + TArray copy(std::move(that)); + that = std::move(*this); + *this = std::move(copy); + } + } + + /** + * Moves all elements of `that` to the end of this array, leaving `that` empty. + * This is a no-op if `that` is empty or equal to this array. + */ + void move_back(TArray& that) { + if (that.empty() || &that == this) { + return; + } + void* dst = this->push_back_raw(that.size()); + // After move() returns, the contents of `dst` will have either been in-place initialized + // using a the move constructor (per-item from `that`'s elements), or will have been + // mem-copied into when MEM_MOVE is true (now valid objects). + that.move(dst); + // All items in `that` have either been destroyed (when MEM_MOVE is false) or should be + // considered invalid (when MEM_MOVE is true). Reset fSize to 0 directly to skip any further + // per-item destruction. + that.changeSize(0); + } + + T* begin() { + return fData; + } + const T* begin() const { + return fData; + } + + // It's safe to use fItemArray + fSize because if fItemArray is nullptr then adding 0 is + // valid and returns nullptr. See [expr.add] in the C++ standard. + T* end() { + if (fData == nullptr) { + SkASSERT(fSize == 0); + } + return fData + fSize; + } + const T* end() const { + if (fData == nullptr) { + SkASSERT(fSize == 0); + } + return fData + fSize; + } + T* data() { return fData; } + const T* data() const { return fData; } + int size() const { return fSize; } + size_t size_bytes() const { return Bytes(fSize); } + void resize(size_t count) { this->resize_back((int)count); } + + void clear() { + this->destroyAll(); + this->changeSize(0); + } + + void shrink_to_fit() { + if (!fOwnMemory || fSize == fCapacity) { + return; + } + this->unpoison(); + if (fSize == 0) { + sk_free(fData); + fData = nullptr; + fCapacity = 0; + } else { + SkSpan allocation = Allocate(fSize); + this->move(TCast(allocation.data())); + if (fOwnMemory) { + sk_free(fData); + } + // Poison is applied in `setDataFromBytes`. + this->setDataFromBytes(allocation); + } + } + + /** + * Get the i^th element. + */ + T& operator[] (int i) { + return fData[sk_collection_check_bounds(i, this->size())]; + } + + const T& operator[] (int i) const { + return fData[sk_collection_check_bounds(i, this->size())]; + } + + T& at(int i) { return (*this)[i]; } + const T& at(int i) const { return (*this)[i]; } + + /** + * equivalent to operator[](0) + */ + T& front() { + sk_collection_not_empty(this->empty()); + return fData[0]; + } + + const T& front() const { + sk_collection_not_empty(this->empty()); + return fData[0]; + } + + /** + * equivalent to operator[](size() - 1) + */ + T& back() { + sk_collection_not_empty(this->empty()); + return fData[fSize - 1]; + } + + const T& back() const { + sk_collection_not_empty(this->empty()); + return fData[fSize - 1]; + } + + /** + * equivalent to operator[](size()-1-i) + */ + T& fromBack(int i) { + return (*this)[fSize - i - 1]; + } + + const T& fromBack(int i) const { + return (*this)[fSize - i - 1]; + } + + bool operator==(const TArray& right) const { + int leftCount = this->size(); + if (leftCount != right.size()) { + return false; + } + for (int index = 0; index < leftCount; ++index) { + if (fData[index] != right.fData[index]) { + return false; + } + } + return true; + } + + bool operator!=(const TArray& right) const { + return !(*this == right); + } + + int capacity() const { + return fCapacity; + } + +protected: + // Creates an empty array that will use the passed storage block until it is insufficiently + // large to hold the entire array. + template + TArray(SkAlignedSTStorage* storage, int size = 0) { + static_assert(InitialCapacity >= 0); + SkASSERT(size >= 0); + SkASSERT(storage->get() != nullptr); + if (size > InitialCapacity) { + this->initData(size); + } else { + this->setDataFromBytes(*storage); + this->changeSize(size); + + // setDataFromBytes always sets fOwnMemory to true, but we are actually using static + // storage here, which shouldn't ever be freed. + fOwnMemory = false; + } + } + + // Copy a C array, using pre-allocated storage if preAllocCount >= count. Otherwise, storage + // will only be used when array shrinks to fit. + template + TArray(const T* array, int size, SkAlignedSTStorage* storage) + : TArray{storage, size} { + this->copy(array); + } + +private: + // Growth factors for checkRealloc. + static constexpr double kExactFit = 1.0; + static constexpr double kGrowing = 1.5; + + static constexpr int kMinHeapAllocCount = 8; + static_assert(SkIsPow2(kMinHeapAllocCount), "min alloc count not power of two."); + + // Note for 32-bit machines kMaxCapacity will be <= SIZE_MAX. For 64-bit machines it will + // just be INT_MAX if the sizeof(T) < 2^32. + static constexpr int kMaxCapacity = SkToInt(std::min(SIZE_MAX / sizeof(T), (size_t)INT_MAX)); + + void setDataFromBytes(SkSpan allocation) { + T* data = TCast(allocation.data()); + // We have gotten extra bytes back from the allocation limit, pin to kMaxCapacity. It + // would seem like the SkContainerAllocator should handle the divide, but it would have + // to a full divide instruction. If done here the size is known at compile, and usually + // can be implemented by a right shift. The full divide takes ~50X longer than the shift. + size_t size = std::min(allocation.size() / sizeof(T), SkToSizeT(kMaxCapacity)); + this->setData(SkSpan(data, size)); + } + + void setData(SkSpan array) { + this->unpoison(); + + fData = array.data(); + fCapacity = SkToU32(array.size()); + fOwnMemory = true; + + this->poison(); + } + + void unpoison() { +#ifdef SK_SANITIZE_ADDRESS + if (fData) { + // SkDebugf("UNPOISONING %p : 0 -> %zu\n", fData, Bytes(fCapacity)); + sk_asan_unpoison_memory_region(this->begin(), Bytes(fCapacity)); + } +#endif + } + + void poison() { +#ifdef SK_SANITIZE_ADDRESS + if (fData && fCapacity > fSize) { + // SkDebugf(" POISONING %p : %zu -> %zu\n", fData, Bytes(fSize), Bytes(fCapacity)); + sk_asan_poison_memory_region(this->end(), Bytes(fCapacity - fSize)); + } +#endif + } + + void changeSize(int n) { + this->unpoison(); + fSize = n; + this->poison(); + } + + // We disable Control-Flow Integrity sanitization (go/cfi) when casting item-array buffers. + // CFI flags this code as dangerous because we are casting `buffer` to a T* while the buffer's + // contents might still be uninitialized memory. When T has a vtable, this is especially risky + // because we could hypothetically access a virtual method on fItemArray and jump to an + // unpredictable location in memory. Of course, TArray won't actually use fItemArray in this + // way, and we don't want to construct a T before the user requests one. There's no real risk + // here, so disable CFI when doing these casts. + SK_CLANG_NO_SANITIZE("cfi") + static T* TCast(void* buffer) { + return (T*)buffer; + } + + static size_t Bytes(int n) { + SkASSERT(n <= kMaxCapacity); + return SkToSizeT(n) * sizeof(T); + } + + static SkSpan Allocate(int capacity, double growthFactor = 1.0) { + return SkContainerAllocator{sizeof(T), kMaxCapacity}.allocate(capacity, growthFactor); + } + + void initData(int count) { + this->setDataFromBytes(Allocate(count)); + this->changeSize(count); + } + + void destroyAll() { + if (!this->empty()) { + T* cursor = this->begin(); + T* const end = this->end(); + do { + cursor->~T(); + cursor++; + } while (cursor < end); + } + } + + /** In the following move and copy methods, 'dst' is assumed to be uninitialized raw storage. + * In the following move methods, 'src' is destroyed leaving behind uninitialized raw storage. + */ + void copy(const T* src) { + if constexpr (std::is_trivially_copyable_v) { + if (!this->empty() && src != nullptr) { + sk_careful_memcpy(fData, src, this->size_bytes()); + } + } else { + for (int i = 0; i < this->size(); ++i) { + new (fData + i) T(src[i]); + } + } + } + + void move(int dst, int src) { + if constexpr (MEM_MOVE) { + memcpy(static_cast(&fData[dst]), + static_cast(&fData[src]), + sizeof(T)); + } else { + new (&fData[dst]) T(std::move(fData[src])); + fData[src].~T(); + } + } + + void move(void* dst) { + if constexpr (MEM_MOVE) { + sk_careful_memcpy(dst, fData, Bytes(fSize)); + } else { + for (int i = 0; i < this->size(); ++i) { + new (static_cast(dst) + Bytes(i)) T(std::move(fData[i])); + fData[i].~T(); + } + } + } + + // Helper function that makes space for n objects, adjusts the count, but does not initialize + // the new objects. + void* push_back_raw(int n) { + this->checkRealloc(n, kGrowing); + void* ptr = fData + fSize; + this->changeSize(fSize + n); + return ptr; + } + + template + SK_ALWAYS_INLINE T* growAndConstructAtEnd(Args&&... args) { + SkSpan buffer = this->preallocateNewData(/*delta=*/1, kGrowing); + T* newT = new (TCast(buffer.data()) + fSize) T(std::forward(args)...); + this->installDataAndUpdateCapacity(buffer); + + return newT; + } + + void checkRealloc(int delta, double growthFactor) { + SkASSERT(delta >= 0); + SkASSERT(fSize >= 0); + SkASSERT(fCapacity >= 0); + + // Check if there are enough remaining allocated elements to satisfy the request. + if (this->capacity() - fSize < delta) { + // Looks like we need to reallocate. + this->installDataAndUpdateCapacity(this->preallocateNewData(delta, growthFactor)); + } + } + + SkSpan preallocateNewData(int delta, double growthFactor) { + SkASSERT(delta >= 0); + SkASSERT(fSize >= 0); + SkASSERT(fCapacity >= 0); + + // Don't overflow fSize or size_t later in the memory allocation. Overflowing memory + // allocation really only applies to fSizes on 32-bit machines; on 64-bit machines this + // will probably never produce a check. Since kMaxCapacity is bounded above by INT_MAX, + // this also checks the bounds of fSize. + if (delta > kMaxCapacity - fSize) { + sk_report_container_overflow_and_die(); + } + const int newCount = fSize + delta; + + return Allocate(newCount, growthFactor); + } + + void installDataAndUpdateCapacity(SkSpan allocation) { + this->move(TCast(allocation.data())); + if (fOwnMemory) { + sk_free(fData); + } + this->setDataFromBytes(allocation); + SkASSERT(fData != nullptr); + } + + T* fData{nullptr}; + int fSize{0}; + uint32_t fOwnMemory : 1; + uint32_t fCapacity : 31; +}; + +template static inline void swap(TArray& a, TArray& b) { + a.swap(b); +} + +// Subclass of TArray that contains a pre-allocated memory block for the array. +template > +class STArray : private SkAlignedSTStorage(Nreq), T>, + public TArray { + // We round up the requested array size to the next capacity multiple. + // This space would likely otherwise go to waste. + static constexpr int N = SkContainerAllocator::RoundUp(Nreq); + static_assert(Nreq > 0); + static_assert(N >= Nreq); + + using Storage = SkAlignedSTStorage; + +public: + STArray() + : Storage{} + , TArray(this) {} // Must use () to avoid confusion with initializer_list + // when T=bool because * are convertable to bool. + + STArray(const T* array, int count) + : Storage{} + , TArray{array, count, this} {} + + STArray(std::initializer_list data) + : STArray{data.begin(), SkToInt(data.size())} {} + + explicit STArray(int reserveCount) + : STArray() { this->reserve_exact(reserveCount); } + + STArray(const STArray& that) + : STArray() { *this = that; } + + explicit STArray(const TArray& that) + : STArray() { *this = that; } + + STArray(STArray&& that) + : STArray() { *this = std::move(that); } + + explicit STArray(TArray&& that) + : STArray() { *this = std::move(that); } + + STArray& operator=(const STArray& that) { + TArray::operator=(that); + return *this; + } + + STArray& operator=(const TArray& that) { + TArray::operator=(that); + return *this; + } + + STArray& operator=(STArray&& that) { + TArray::operator=(std::move(that)); + return *this; + } + + STArray& operator=(TArray&& that) { + TArray::operator=(std::move(that)); + return *this; + } + + // Force the use of TArray for data() and size(). + using TArray::data; + using TArray::size; +}; +} // namespace skia_private +#endif // SkTArray_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTDArray.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTDArray.h new file mode 100644 index 00000000000..fef454bdc44 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTDArray.h @@ -0,0 +1,235 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTDArray_DEFINED +#define SkTDArray_DEFINED + +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkAssert.h" +#include "include/private/base/SkDebug.h" +#include "include/private/base/SkTo.h" + +#include +#include +#include + +class SK_SPI SkTDStorage { +public: + explicit SkTDStorage(int sizeOfT); + SkTDStorage(const void* src, int size, int sizeOfT); + + // Copy + SkTDStorage(const SkTDStorage& that); + SkTDStorage& operator= (const SkTDStorage& that); + + // Move + SkTDStorage(SkTDStorage&& that); + SkTDStorage& operator= (SkTDStorage&& that); + + ~SkTDStorage(); + + void reset(); + void swap(SkTDStorage& that); + + // Size routines + bool empty() const { return fSize == 0; } + void clear() { fSize = 0; } + int size() const { return fSize; } + void resize(int newSize); + size_t size_bytes() const { return this->bytes(fSize); } + + // Capacity routines + int capacity() const { return fCapacity; } + void reserve(int newCapacity); + void shrink_to_fit(); + + void* data() { return fStorage; } + const void* data() const { return fStorage; } + + // Deletion routines + void erase(int index, int count); + // Removes the entry at 'index' and replaces it with the last array element + void removeShuffle(int index); + + // Insertion routines + void* prepend(); + + void append(); + void append(int count); + void* append(const void* src, int count); + + void* insert(int index); + void* insert(int index, int count, const void* src); + + void pop_back() { + SkASSERT(fSize > 0); + fSize--; + } + + friend bool operator==(const SkTDStorage& a, const SkTDStorage& b); + friend bool operator!=(const SkTDStorage& a, const SkTDStorage& b) { + return !(a == b); + } + +private: + size_t bytes(int n) const { return SkToSizeT(n * fSizeOfT); } + void* address(int n) { return fStorage + this->bytes(n); } + + // Adds delta to fSize. Crash if outside [0, INT_MAX] + int calculateSizeOrDie(int delta); + + // Move the tail of the array defined by the indexes tailStart and tailEnd to dstIndex. The + // elements at dstIndex are overwritten by the tail. + void moveTail(int dstIndex, int tailStart, int tailEnd); + + // Copy src into the array at dstIndex. + void copySrc(int dstIndex, const void* src, int count); + + const int fSizeOfT; + std::byte* fStorage{nullptr}; + int fCapacity{0}; // size of the allocation in fArray (#elements) + int fSize{0}; // logical number of elements (fSize <= fCapacity) +}; + +static inline void swap(SkTDStorage& a, SkTDStorage& b) { + a.swap(b); +} + +// SkTDArray implements a std::vector-like array for raw data-only objects that do not require +// construction or destruction. The constructor and destructor for T will not be called; T objects +// will always be moved via raw memcpy. Newly created T objects will contain uninitialized memory. +template class SkTDArray { +public: + SkTDArray() : fStorage{sizeof(T)} {} + SkTDArray(const T src[], int count) : fStorage{src, count, sizeof(T)} { } + SkTDArray(const std::initializer_list& list) : SkTDArray(list.begin(), list.size()) {} + + // Copy + SkTDArray(const SkTDArray& src) : SkTDArray(src.data(), src.size()) {} + SkTDArray& operator=(const SkTDArray& src) { + fStorage = src.fStorage; + return *this; + } + + // Move + SkTDArray(SkTDArray&& src) : fStorage{std::move(src.fStorage)} {} + SkTDArray& operator=(SkTDArray&& src) { + fStorage = std::move(src.fStorage); + return *this; + } + + friend bool operator==(const SkTDArray& a, const SkTDArray& b) { + return a.fStorage == b.fStorage; + } + friend bool operator!=(const SkTDArray& a, const SkTDArray& b) { return !(a == b); } + + void swap(SkTDArray& that) { + using std::swap; + swap(fStorage, that.fStorage); + } + + bool empty() const { return fStorage.empty(); } + + // Return the number of elements in the array + int size() const { return fStorage.size(); } + + // Return the total number of elements allocated. + // Note: capacity() - size() gives you the number of elements you can add without causing an + // allocation. + int capacity() const { return fStorage.capacity(); } + + // return the number of bytes in the array: count * sizeof(T) + size_t size_bytes() const { return fStorage.size_bytes(); } + + T* data() { return static_cast(fStorage.data()); } + const T* data() const { return static_cast(fStorage.data()); } + T* begin() { return this->data(); } + const T* begin() const { return this->data(); } + T* end() { return this->data() + this->size(); } + const T* end() const { return this->data() + this->size(); } + + T& operator[](int index) { + return this->data()[sk_collection_check_bounds(index, this->size())]; + } + const T& operator[](int index) const { + return this->data()[sk_collection_check_bounds(index, this->size())]; + } + + const T& back() const { + sk_collection_not_empty(this->empty()); + return this->data()[this->size() - 1]; + } + T& back() { + sk_collection_not_empty(this->empty()); + return this->data()[this->size() - 1]; + } + + void reset() { + fStorage.reset(); + } + + void clear() { + fStorage.clear(); + } + + // Sets the number of elements in the array. + // If the array does not have space for count elements, it will increase + // the storage allocated to some amount greater than that required. + // It will never shrink the storage. + void resize(int count) { + fStorage.resize(count); + } + + void reserve(int n) { + fStorage.reserve(n); + } + + T* append() { + fStorage.append(); + return this->end() - 1; + } + T* append(int count) { + fStorage.append(count); + return this->end() - count; + } + T* append(int count, const T* src) { + return static_cast(fStorage.append(src, count)); + } + + T* insert(int index) { + return static_cast(fStorage.insert(index)); + } + T* insert(int index, int count, const T* src = nullptr) { + return static_cast(fStorage.insert(index, count, src)); + } + + void remove(int index, int count = 1) { + fStorage.erase(index, count); + } + + void removeShuffle(int index) { + fStorage.removeShuffle(index); + } + + // routines to treat the array like a stack + void push_back(const T& v) { + this->append(); + this->back() = v; + } + void pop_back() { fStorage.pop_back(); } + + void shrink_to_fit() { + fStorage.shrink_to_fit(); + } + +private: + SkTDStorage fStorage; +}; + +template static inline void swap(SkTDArray& a, SkTDArray& b) { a.swap(b); } + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTFitsIn.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTFitsIn.h new file mode 100644 index 00000000000..365748abef4 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTFitsIn.h @@ -0,0 +1,105 @@ +/* + * Copyright 2013 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTFitsIn_DEFINED +#define SkTFitsIn_DEFINED + +#include "include/private/base/SkDebug.h" + +#include +#include +#include + +/** + * std::underlying_type is only defined for enums. For integral types, we just want the type. + */ +template +struct sk_strip_enum { + typedef T type; +}; + +template +struct sk_strip_enum::value>::type> { + typedef typename std::underlying_type::type type; +}; + + +/** + * In C++ an unsigned to signed cast where the source value cannot be represented in the destination + * type results in an implementation defined destination value. Unlike C, C++ does not allow a trap. + * This makes "(S)(D)s == s" a possibly useful test. However, there are two cases where this is + * incorrect: + * + * when testing if a value of a smaller signed type can be represented in a larger unsigned type + * (int8_t)(uint16_t)-1 == -1 => (int8_t)0xFFFF == -1 => [implementation defined] == -1 + * + * when testing if a value of a larger unsigned type can be represented in a smaller signed type + * (uint16_t)(int8_t)0xFFFF == 0xFFFF => (uint16_t)-1 == 0xFFFF => 0xFFFF == 0xFFFF => true. + * + * Consider the cases: + * u = unsigned, less digits + * U = unsigned, more digits + * s = signed, less digits + * S = signed, more digits + * v is the value we're considering. + * + * u -> U: (u)(U)v == v, trivially true + * U -> u: (U)(u)v == v, both casts well defined, test works + * s -> S: (s)(S)v == v, trivially true + * S -> s: (S)(s)v == v, first cast implementation value, second cast defined, test works + * s -> U: (s)(U)v == v, *this is bad*, the second cast results in implementation defined value + * S -> u: (S)(u)v == v, the second cast is required to prevent promotion of rhs to unsigned + * u -> S: (u)(S)v == v, trivially true + * U -> s: (U)(s)v == v, *this is bad*, + * first cast results in implementation defined value, + * second cast is defined. However, this creates false positives + * uint16_t x = 0xFFFF + * (uint16_t)(int8_t)x == x + * => (uint16_t)-1 == x + * => 0xFFFF == x + * => true + * + * So for the eight cases three are trivially true, three more are valid casts, and two are special. + * The two 'full' checks which otherwise require two comparisons are valid cast checks. + * The two remaining checks s -> U [v >= 0] and U -> s [v <= max(s)] can be done with one op. + */ + +template +static constexpr inline +typename std::enable_if<(std::is_integral::value || std::is_enum::value) && + (std::is_integral::value || std::is_enum::value), bool>::type +/*bool*/ SkTFitsIn(S src) { + // Ensure that is_signed and is_unsigned are passed the arithmetic underlyng types of enums. + using Sa = typename sk_strip_enum::type; + using Da = typename sk_strip_enum::type; + + // SkTFitsIn() is used in public headers, so needs to be written targeting at most C++11. + return + + // E.g. (int8_t)(uint8_t) int8_t(-1) == -1, but the uint8_t == 255, not -1. + (std::is_signed::value && std::is_unsigned::value && sizeof(Sa) <= sizeof(Da)) ? + (S)0 <= src : + + // E.g. (uint8_t)(int8_t) uint8_t(255) == 255, but the int8_t == -1. + (std::is_signed::value && std::is_unsigned::value && sizeof(Da) <= sizeof(Sa)) ? + src <= (S)std::numeric_limits::max() : + +#if !defined(SK_DEBUG) && !defined(__MSVC_RUNTIME_CHECKS ) + // Correct (simple) version. This trips up MSVC's /RTCc run-time checking. + (S)(D)src == src; +#else + // More complex version that's safe with /RTCc. Used in all debug builds, for coverage. + (std::is_signed::value) ? + (intmax_t)src >= (intmax_t)std::numeric_limits::min() && + (intmax_t)src <= (intmax_t)std::numeric_limits::max() : + + // std::is_unsigned ? + (uintmax_t)src <= (uintmax_t)std::numeric_limits::max(); +#endif +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTLogic.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTLogic.h new file mode 100644 index 00000000000..26f363c9469 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTLogic.h @@ -0,0 +1,56 @@ +/* + * Copyright 2013 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * + * This header provides some std:: features early in the skstd namespace + * and several Skia-specific additions in the sknonstd namespace. + */ + +#ifndef SkTLogic_DEFINED +#define SkTLogic_DEFINED + +#include +#include +#include "include/private/base/SkTo.h" + +// The sknonstd namespace contains things we would like to be proposed and feel std-ish. +namespace sknonstd { + +// The name 'copy' here is fraught with peril. In this case it means 'append', not 'overwrite'. +// Alternate proposed names are 'propagate', 'augment', or 'append' (and 'add', but already taken). +// std::experimental::propagate_const already exists for other purposes in TSv2. +// These also follow the pattern used by boost. +template struct copy_const { + using type = std::conditional_t::value, std::add_const_t, D>; +}; +template using copy_const_t = typename copy_const::type; + +template struct copy_volatile { + using type = std::conditional_t::value, std::add_volatile_t, D>; +}; +template using copy_volatile_t = typename copy_volatile::type; + +template struct copy_cv { + using type = copy_volatile_t, S>; +}; +template using copy_cv_t = typename copy_cv::type; + +// The name 'same' here means 'overwrite'. +// Alternate proposed names are 'replace', 'transfer', or 'qualify_from'. +// same_xxx can be written as copy_xxx, S> +template using same_const = copy_const, S>; +template using same_const_t = typename same_const::type; +template using same_volatile =copy_volatile,S>; +template using same_volatile_t = typename same_volatile::type; +template using same_cv = copy_cv, S>; +template using same_cv_t = typename same_cv::type; + +} // namespace sknonstd + +template +constexpr int SkCount(const Container& c) { return SkTo(std::size(c)); } + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTPin.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTPin.h new file mode 100644 index 00000000000..c824c446403 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTPin.h @@ -0,0 +1,23 @@ +/* + * Copyright 2020 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTPin_DEFINED +#define SkTPin_DEFINED + +#include + +/** @return x pinned (clamped) between lo and hi, inclusively. + + Unlike std::clamp(), SkTPin() always returns a value between lo and hi. + If x is NaN, SkTPin() returns lo but std::clamp() returns NaN. +*/ +template +static constexpr const T& SkTPin(const T& x, const T& lo, const T& hi) { + return std::max(lo, std::min(x, hi)); +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTemplates.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTemplates.h new file mode 100644 index 00000000000..3c9ca551253 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTemplates.h @@ -0,0 +1,446 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTemplates_DEFINED +#define SkTemplates_DEFINED + +#include "include/private/base/SkAlign.h" +#include "include/private/base/SkAssert.h" +#include "include/private/base/SkDebug.h" +#include "include/private/base/SkMalloc.h" +#include "include/private/base/SkTLogic.h" +#include "include/private/base/SkTo.h" + +#include +#include +#include +#include +#include +#include +#include + + +/** \file SkTemplates.h + + This file contains light-weight template classes for type-safe and exception-safe + resource management. +*/ + +/** + * Marks a local variable as known to be unused (to avoid warnings). + * Note that this does *not* prevent the local variable from being optimized away. + */ +template inline void sk_ignore_unused_variable(const T&) { } + +/** + * This is a general purpose absolute-value function. + * See SkAbs32 in (SkSafe32.h) for a 32-bit int specific version that asserts. + */ +template static inline T SkTAbs(T value) { + if (value < 0) { + value = -value; + } + return value; +} + +/** + * Returns a pointer to a D which comes immediately after S[count]. + */ +template inline D* SkTAfter(S* ptr, size_t count = 1) { + return reinterpret_cast(ptr + count); +} + +/** + * Returns a pointer to a D which comes byteOffset bytes after S. + */ +template inline D* SkTAddOffset(S* ptr, ptrdiff_t byteOffset) { + // The intermediate char* has the same cv-ness as D as this produces better error messages. + // This relies on the fact that reinterpret_cast can add constness, but cannot remove it. + return reinterpret_cast(reinterpret_cast*>(ptr) + byteOffset); +} + +template struct SkOverloadedFunctionObject { + template + auto operator()(Args&&... args) const -> decltype(P(std::forward(args)...)) { + return P(std::forward(args)...); + } +}; + +template using SkFunctionObject = + SkOverloadedFunctionObject, F>; + +/** \class SkAutoTCallVProc + + Call a function when this goes out of scope. The template uses two + parameters, the object, and a function that is to be called in the destructor. + If release() is called, the object reference is set to null. If the object + reference is null when the destructor is called, we do not call the + function. +*/ +template class SkAutoTCallVProc + : public std::unique_ptr> { + using inherited = std::unique_ptr>; +public: + using inherited::inherited; + SkAutoTCallVProc(const SkAutoTCallVProc&) = delete; + SkAutoTCallVProc(SkAutoTCallVProc&& that) : inherited(std::move(that)) {} + + operator T*() const { return this->get(); } +}; + + +namespace skia_private { +/** Allocate an array of T elements, and free the array in the destructor + */ +template class AutoTArray { +public: + AutoTArray() {} + // Allocate size number of T elements + explicit AutoTArray(size_t size) { + fSize = check_size_bytes_too_big(size); + fData.reset(size > 0 ? new T[size] : nullptr); + } + + // TODO: remove when all uses are gone. + explicit AutoTArray(int size) : AutoTArray(SkToSizeT(size)) {} + + AutoTArray(AutoTArray&& other) : fData(std::move(other.fData)) { + fSize = std::exchange(other.fSize, 0); + } + AutoTArray& operator=(AutoTArray&& other) { + if (this != &other) { + fData = std::move(other.fData); + fSize = std::exchange(other.fSize, 0); + } + return *this; + } + + // Reallocates given a new count. Reallocation occurs even if new count equals old count. + void reset(size_t count = 0) { + *this = AutoTArray(count); + } + + T* get() const { return fData.get(); } + + T& operator[](size_t index) const { + return fData[sk_collection_check_bounds(index, fSize)]; + } + + const T* data() const { return fData.get(); } + T* data() { return fData.get(); } + + size_t size() const { return fSize; } + bool empty() const { return fSize == 0; } + size_t size_bytes() const { return sizeof(T) * fSize; } + + T* begin() { + return fData; + } + const T* begin() const { + return fData; + } + + // It's safe to use fItemArray + fSize because if fItemArray is nullptr then adding 0 is + // valid and returns nullptr. See [expr.add] in the C++ standard. + T* end() { + if (fData == nullptr) { + SkASSERT(fSize == 0); + } + return fData + fSize; + } + const T* end() const { + if (fData == nullptr) { + SkASSERT(fSize == 0); + } + return fData + fSize; + } + +private: + std::unique_ptr fData; + size_t fSize = 0; +}; + +/** Wraps AutoTArray, with room for kCountRequested elements preallocated. + */ +template class AutoSTArray { +public: + AutoSTArray(AutoSTArray&&) = delete; + AutoSTArray(const AutoSTArray&) = delete; + AutoSTArray& operator=(AutoSTArray&&) = delete; + AutoSTArray& operator=(const AutoSTArray&) = delete; + + /** Initialize with no objects */ + AutoSTArray() { + fArray = nullptr; + fCount = 0; + } + + /** Allocate count number of T elements + */ + AutoSTArray(int count) { + fArray = nullptr; + fCount = 0; + this->reset(count); + } + + ~AutoSTArray() { + this->reset(0); + } + + /** Destroys previous objects in the array and default constructs count number of objects */ + void reset(int count) { + T* start = fArray; + T* iter = start + fCount; + while (iter > start) { + (--iter)->~T(); + } + + SkASSERT(count >= 0); + if (fCount != count) { + if (fCount > kCount) { + // 'fArray' was allocated last time so free it now + SkASSERT((T*) fStorage != fArray); + sk_free(fArray); + } + + if (count > kCount) { + fArray = (T*) sk_malloc_throw(count, sizeof(T)); + } else if (count > 0) { + fArray = (T*) fStorage; + } else { + fArray = nullptr; + } + + fCount = count; + } + + iter = fArray; + T* stop = fArray + count; + while (iter < stop) { + new (iter++) T; + } + } + + /** Return the number of T elements in the array + */ + int count() const { return fCount; } + + /** Return the array of T elements. Will be NULL if count == 0 + */ + T* get() const { return fArray; } + + T* begin() { return fArray; } + + const T* begin() const { return fArray; } + + T* end() { return fArray + fCount; } + + const T* end() const { return fArray + fCount; } + + /** Return the nth element in the array + */ + T& operator[](int index) const { + return fArray[sk_collection_check_bounds(index, fCount)]; + } + + /** Aliases matching other types, like std::vector. */ + const T* data() const { return fArray; } + T* data() { return fArray; } + size_t size() const { return fCount; } + +private: +#if defined(SK_BUILD_FOR_GOOGLE3) + // Stack frame size is limited for SK_BUILD_FOR_GOOGLE3. 4k is less than the actual max, + // but some functions have multiple large stack allocations. + static const int kMaxBytes = 4 * 1024; + static const int kCount = kCountRequested * sizeof(T) > kMaxBytes + ? kMaxBytes / sizeof(T) + : kCountRequested; +#else + static const int kCount = kCountRequested; +#endif + + int fCount; + T* fArray; + alignas(T) char fStorage[kCount * sizeof(T)]; +}; + +/** Manages an array of T elements, freeing the array in the destructor. + * Does NOT call any constructors/destructors on T (T must be POD). + */ +template ::value && + std::is_trivially_destructible::value>> +class AutoTMalloc { +public: + /** Takes ownership of the ptr. The ptr must be a value which can be passed to sk_free. */ + explicit AutoTMalloc(T* ptr = nullptr) : fPtr(ptr) {} + + /** Allocates space for 'count' Ts. */ + explicit AutoTMalloc(size_t count) + : fPtr(count ? (T*)sk_malloc_throw(count, sizeof(T)) : nullptr) {} + + AutoTMalloc(AutoTMalloc&&) = default; + AutoTMalloc& operator=(AutoTMalloc&&) = default; + + /** Resize the memory area pointed to by the current ptr preserving contents. */ + void realloc(size_t count) { + fPtr.reset(count ? (T*)sk_realloc_throw(fPtr.release(), count * sizeof(T)) : nullptr); + } + + /** Resize the memory area pointed to by the current ptr without preserving contents. */ + T* reset(size_t count = 0) { + fPtr.reset(count ? (T*)sk_malloc_throw(count, sizeof(T)) : nullptr); + return this->get(); + } + + T* get() const { return fPtr.get(); } + + operator T*() { return fPtr.get(); } + + operator const T*() const { return fPtr.get(); } + + T& operator[](int index) { return fPtr.get()[index]; } + + const T& operator[](int index) const { return fPtr.get()[index]; } + + /** Aliases matching other types, like std::vector. */ + const T* data() const { return fPtr.get(); } + T* data() { return fPtr.get(); } + + /** + * Transfer ownership of the ptr to the caller, setting the internal + * pointer to NULL. Note that this differs from get(), which also returns + * the pointer, but it does not transfer ownership. + */ + T* release() { return fPtr.release(); } + +private: + std::unique_ptr> fPtr; +}; + +template ::value && + std::is_trivially_destructible::value>> +class AutoSTMalloc { +public: + AutoSTMalloc() : fPtr(fTStorage) {} + + AutoSTMalloc(size_t count) { + if (count > kCount) { + fPtr = (T*)sk_malloc_throw(count, sizeof(T)); + } else if (count) { + fPtr = fTStorage; + } else { + fPtr = nullptr; + } + } + + AutoSTMalloc(AutoSTMalloc&&) = delete; + AutoSTMalloc(const AutoSTMalloc&) = delete; + AutoSTMalloc& operator=(AutoSTMalloc&&) = delete; + AutoSTMalloc& operator=(const AutoSTMalloc&) = delete; + + ~AutoSTMalloc() { + if (fPtr != fTStorage) { + sk_free(fPtr); + } + } + + // doesn't preserve contents + T* reset(size_t count) { + if (fPtr != fTStorage) { + sk_free(fPtr); + } + if (count > kCount) { + fPtr = (T*)sk_malloc_throw(count, sizeof(T)); + } else if (count) { + fPtr = fTStorage; + } else { + fPtr = nullptr; + } + return fPtr; + } + + T* get() const { return fPtr; } + + operator T*() { + return fPtr; + } + + operator const T*() const { + return fPtr; + } + + T& operator[](int index) { + return fPtr[index]; + } + + const T& operator[](int index) const { + return fPtr[index]; + } + + /** Aliases matching other types, like std::vector. */ + const T* data() const { return fPtr; } + T* data() { return fPtr; } + + // Reallocs the array, can be used to shrink the allocation. Makes no attempt to be intelligent + void realloc(size_t count) { + if (count > kCount) { + if (fPtr == fTStorage) { + fPtr = (T*)sk_malloc_throw(count, sizeof(T)); + memcpy((void*)fPtr, fTStorage, kCount * sizeof(T)); + } else { + fPtr = (T*)sk_realloc_throw(fPtr, count, sizeof(T)); + } + } else if (count) { + if (fPtr != fTStorage) { + fPtr = (T*)sk_realloc_throw(fPtr, count, sizeof(T)); + } + } else { + this->reset(0); + } + } + +private: + // Since we use uint32_t storage, we might be able to get more elements for free. + static const size_t kCountWithPadding = SkAlign4(kCountRequested*sizeof(T)) / sizeof(T); +#if defined(SK_BUILD_FOR_GOOGLE3) + // Stack frame size is limited for SK_BUILD_FOR_GOOGLE3. 4k is less than the actual max, but some functions + // have multiple large stack allocations. + static const size_t kMaxBytes = 4 * 1024; + static const size_t kCount = kCountRequested * sizeof(T) > kMaxBytes + ? kMaxBytes / sizeof(T) + : kCountWithPadding; +#else + static const size_t kCount = kCountWithPadding; +#endif + + T* fPtr; + union { + uint32_t fStorage32[SkAlign4(kCount*sizeof(T)) >> 2]; + T fTStorage[1]; // do NOT want to invoke T::T() + }; +}; + +using UniqueVoidPtr = std::unique_ptr>; + +} // namespace skia_private + +template +constexpr auto SkMakeArrayFromIndexSequence(C c, std::index_sequence is) +-> std::array())), sizeof...(Is)> { + return {{ c(Is)... }}; +} + +template constexpr auto SkMakeArray(C c) +-> std::array::value_type>())), N> { + return SkMakeArrayFromIndexSequence(c, std::make_index_sequence{}); +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkThreadAnnotations.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkThreadAnnotations.h new file mode 100644 index 00000000000..a67fbaaca08 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkThreadAnnotations.h @@ -0,0 +1,104 @@ +/* + * Copyright 2019 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkThreadAnnotations_DEFINED +#define SkThreadAnnotations_DEFINED + +#include "include/private/base/SkFeatures.h" // IWYU pragma: keep + +// The bulk of this code is cribbed from: +// http://clang.llvm.org/docs/ThreadSafetyAnalysis.html + +#if defined(__clang__) && (!defined(SWIG)) +#define SK_THREAD_ANNOTATION_ATTRIBUTE(x) __attribute__((x)) +#else +#define SK_THREAD_ANNOTATION_ATTRIBUTE(x) // no-op +#endif + +#define SK_CAPABILITY(x) \ + SK_THREAD_ANNOTATION_ATTRIBUTE(capability(x)) + +#define SK_SCOPED_CAPABILITY \ + SK_THREAD_ANNOTATION_ATTRIBUTE(scoped_lockable) + +#define SK_GUARDED_BY(x) \ + SK_THREAD_ANNOTATION_ATTRIBUTE(guarded_by(x)) + +#define SK_PT_GUARDED_BY(x) \ + SK_THREAD_ANNOTATION_ATTRIBUTE(pt_guarded_by(x)) + +#define SK_ACQUIRED_BEFORE(...) \ + SK_THREAD_ANNOTATION_ATTRIBUTE(acquired_before(__VA_ARGS__)) + +#define SK_ACQUIRED_AFTER(...) \ + SK_THREAD_ANNOTATION_ATTRIBUTE(acquired_after(__VA_ARGS__)) + +#define SK_REQUIRES(...) \ + SK_THREAD_ANNOTATION_ATTRIBUTE(requires_capability(__VA_ARGS__)) + +#define SK_REQUIRES_SHARED(...) \ + SK_THREAD_ANNOTATION_ATTRIBUTE(requires_shared_capability(__VA_ARGS__)) + +#define SK_ACQUIRE(...) \ + SK_THREAD_ANNOTATION_ATTRIBUTE(acquire_capability(__VA_ARGS__)) + +#define SK_ACQUIRE_SHARED(...) \ + SK_THREAD_ANNOTATION_ATTRIBUTE(acquire_shared_capability(__VA_ARGS__)) + +// Would be SK_RELEASE, but that is already in use as SK_DEBUG vs. SK_RELEASE. +#define SK_RELEASE_CAPABILITY(...) \ + SK_THREAD_ANNOTATION_ATTRIBUTE(release_capability(__VA_ARGS__)) + +// For symmetry with SK_RELEASE_CAPABILITY. +#define SK_RELEASE_SHARED_CAPABILITY(...) \ + SK_THREAD_ANNOTATION_ATTRIBUTE(release_shared_capability(__VA_ARGS__)) + +#define SK_TRY_ACQUIRE(...) \ + SK_THREAD_ANNOTATION_ATTRIBUTE(try_acquire_capability(__VA_ARGS__)) + +#define SK_TRY_ACQUIRE_SHARED(...) \ + SK_THREAD_ANNOTATION_ATTRIBUTE(try_acquire_shared_capability(__VA_ARGS__)) + +#define SK_EXCLUDES(...) \ + SK_THREAD_ANNOTATION_ATTRIBUTE(locks_excluded(__VA_ARGS__)) + +#define SK_ASSERT_CAPABILITY(x) \ + SK_THREAD_ANNOTATION_ATTRIBUTE(assert_capability(x)) + +#define SK_ASSERT_SHARED_CAPABILITY(x) \ + SK_THREAD_ANNOTATION_ATTRIBUTE(assert_shared_capability(x)) + +#define SK_RETURN_CAPABILITY(x) \ + SK_THREAD_ANNOTATION_ATTRIBUTE(lock_returned(x)) + +#define SK_NO_THREAD_SAFETY_ANALYSIS \ + SK_THREAD_ANNOTATION_ATTRIBUTE(no_thread_safety_analysis) + +#if defined(SK_BUILD_FOR_GOOGLE3) && !defined(SK_BUILD_FOR_WASM_IN_GOOGLE3) \ + && !defined(SK_BUILD_FOR_WIN) + extern "C" { + void __google_cxa_guard_acquire_begin(void) __attribute__((weak)); + void __google_cxa_guard_acquire_end (void) __attribute__((weak)); + } + static inline void sk_potentially_blocking_region_begin() { + if (&__google_cxa_guard_acquire_begin) { + __google_cxa_guard_acquire_begin(); + } + } + static inline void sk_potentially_blocking_region_end() { + if (&__google_cxa_guard_acquire_end) { + __google_cxa_guard_acquire_end(); + } + } + #define SK_POTENTIALLY_BLOCKING_REGION_BEGIN sk_potentially_blocking_region_begin() + #define SK_POTENTIALLY_BLOCKING_REGION_END sk_potentially_blocking_region_end() +#else + #define SK_POTENTIALLY_BLOCKING_REGION_BEGIN + #define SK_POTENTIALLY_BLOCKING_REGION_END +#endif + +#endif // SkThreadAnnotations_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkThreadID.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkThreadID.h new file mode 100644 index 00000000000..18984884c96 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkThreadID.h @@ -0,0 +1,23 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkThreadID_DEFINED +#define SkThreadID_DEFINED + +#include "include/private/base/SkAPI.h" +#include "include/private/base/SkDebug.h" + +#include + +typedef int64_t SkThreadID; + +// SkMutex.h uses SkGetThreadID in debug only code. +SkDEBUGCODE(SK_SPI) SkThreadID SkGetThreadID(); + +const SkThreadID kIllegalThreadID = 0; + +#endif // SkThreadID_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTo.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTo.h new file mode 100644 index 00000000000..51ccafeeaf7 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTo.h @@ -0,0 +1,39 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkTo_DEFINED +#define SkTo_DEFINED + +#include "include/private/base/SkAssert.h" +#include "include/private/base/SkTFitsIn.h" + +#include +#include + +template constexpr D SkTo(S s) { + return SkASSERT(SkTFitsIn(s)), + static_cast(s); +} + +template constexpr int8_t SkToS8(S x) { return SkTo(x); } +template constexpr uint8_t SkToU8(S x) { return SkTo(x); } +template constexpr int16_t SkToS16(S x) { return SkTo(x); } +template constexpr uint16_t SkToU16(S x) { return SkTo(x); } +template constexpr int32_t SkToS32(S x) { return SkTo(x); } +template constexpr uint32_t SkToU32(S x) { return SkTo(x); } +template constexpr int64_t SkToS64(S x) { return SkTo(x); } +template constexpr uint64_t SkToU64(S x) { return SkTo(x); } +template constexpr int SkToInt(S x) { return SkTo(x); } +template constexpr unsigned SkToUInt(S x) { return SkTo(x); } +template constexpr size_t SkToSizeT(S x) { return SkTo(x); } + +/** @return false or true based on the condition +*/ +template static constexpr bool SkToBool(const T& x) { + return (bool)x; +} + +#endif // SkTo_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTypeTraits.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTypeTraits.h new file mode 100644 index 00000000000..736f7897763 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/base/SkTypeTraits.h @@ -0,0 +1,33 @@ +// Copyright 2022 Google LLC +// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +#ifndef SkTypeTraits_DEFINED +#define SkTypeTraits_DEFINED + +#include +#include + +// Trait for identifying types which are relocatable via memcpy, for container optimizations. +template +struct sk_has_trivially_relocatable_member : std::false_type {}; + +// Types can declare themselves trivially relocatable with a public +// using sk_is_trivially_relocatable = std::true_type; +template +struct sk_has_trivially_relocatable_member> + : T::sk_is_trivially_relocatable {}; + +// By default, all trivially copyable types are trivially relocatable. +template +struct sk_is_trivially_relocatable + : std::disjunction, sk_has_trivially_relocatable_member>{}; + +// Here be some dragons: while technically not guaranteed, we count on all sane unique_ptr +// implementations to be trivially relocatable. +template +struct sk_is_trivially_relocatable> : std::true_type {}; + +template +inline constexpr bool sk_is_trivially_relocatable_v = sk_is_trivially_relocatable::value; + +#endif // SkTypeTraits_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrDeferredDisplayList.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrDeferredDisplayList.h new file mode 100644 index 00000000000..e54b51c43b3 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrDeferredDisplayList.h @@ -0,0 +1,120 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrDeferredDisplayList_DEFINED +#define GrDeferredDisplayList_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" +#include "include/gpu/GrRecordingContext.h" +#include "include/private/base/SkTArray.h" +#include "include/private/chromium/GrSurfaceCharacterization.h" + +class GrDirectContext; +class GrRenderTargetProxy; +class GrRenderTask; +class GrDeferredDisplayListPriv; +class SkSurface; + +/* + * This class contains pre-processed gpu operations that can be replayed into + * an SkSurface via SkSurface::draw(GrDeferredDisplayList*). + */ +class GrDeferredDisplayList : public SkNVRefCnt { +public: + SK_API ~GrDeferredDisplayList(); + + SK_API const GrSurfaceCharacterization& characterization() const { + return fCharacterization; + } + /** + * Iterate through the programs required by the DDL. + */ + class SK_API ProgramIterator { + public: + ProgramIterator(GrDirectContext*, GrDeferredDisplayList*); + ~ProgramIterator(); + + // This returns true if any work was done. Getting a cache hit does not count as work. + bool compile(); + bool done() const; + void next(); + + private: + GrDirectContext* fDContext; + const skia_private::TArray& fProgramData; + int fIndex; + }; + + // Provides access to functions that aren't part of the public API. + GrDeferredDisplayListPriv priv(); + const GrDeferredDisplayListPriv priv() const; // NOLINT(readability-const-return-type) + +private: + friend class GrDrawingManager; // for access to 'fRenderTasks', 'fLazyProxyData', 'fArenas' + friend class GrDeferredDisplayListRecorder; // for access to 'fLazyProxyData' + friend class GrDeferredDisplayListPriv; + + // This object is the source from which the lazy proxy backing the DDL will pull its backing + // texture when the DDL is replayed. It has to be separately ref counted bc the lazy proxy + // can outlive the DDL. + class LazyProxyData : public SkRefCnt { + public: + // Upon being replayed - this field will be filled in (by the DrawingManager) with the + // proxy backing the destination SkSurface. Note that, since there is no good place to + // clear it, it can become a dangling pointer. Additionally, since the renderTargetProxy + // doesn't get a ref here, the SkSurface that owns it must remain alive until the DDL + // is flushed. + // TODO: the drawing manager could ref the renderTargetProxy for the DDL and then add + // a renderingTask to unref it after the DDL's ops have been executed. + GrRenderTargetProxy* fReplayDest = nullptr; + }; + + SK_API GrDeferredDisplayList(const GrSurfaceCharacterization& characterization, + sk_sp fTargetProxy, + sk_sp); + + const skia_private::TArray& programData() const { + return fProgramData; + } + + const GrSurfaceCharacterization fCharacterization; + + // These are ordered such that the destructor cleans op tasks up first (which may refer back + // to the arena and memory pool in their destructors). + GrRecordingContext::OwnedArenas fArenas; + skia_private::TArray> fRenderTasks; + + skia_private::TArray fProgramData; + sk_sp fTargetProxy; + sk_sp fLazyProxyData; +}; + +namespace skgpu::ganesh { +/** Draws the deferred display list created via a GrDeferredDisplayListRecorder. + If the deferred display list is not compatible with the surface, the draw is skipped + and false is return. + + The xOffset and yOffset parameters are experimental and, if not both zero, will cause + the draw to be ignored. + When implemented, if xOffset or yOffset are non-zero, the DDL will be drawn offset by that + amount into the surface. + + @param SkSurface The surface to apply the commands to, cannot be nullptr. + @param ddl drawing commands, cannot be nullptr. + @return false if ddl is not compatible + + example: https://fiddle.skia.org/c/@Surface_draw_2 +*/ +SK_API bool DrawDDL(SkSurface*, + sk_sp ddl); + +SK_API bool DrawDDL(sk_sp, + sk_sp ddl); +} + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrDeferredDisplayListRecorder.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrDeferredDisplayListRecorder.h new file mode 100644 index 00000000000..f66cb94d0cd --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrDeferredDisplayListRecorder.h @@ -0,0 +1,62 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrDeferredDisplayListRecorder_DEFINED +#define GrDeferredDisplayListRecorder_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" +#include "include/private/chromium/GrDeferredDisplayList.h" +#include "include/private/chromium/GrSurfaceCharacterization.h" + +class GrRecordingContext; +class GrRenderTargetProxy; +class SkCanvas; +class SkSurface; + +/* + * This class is intended to be used as: + * Get a GrSurfaceCharacterization representing the intended gpu-backed destination SkSurface + * Create one of these (a GrDeferredDisplayListRecorder) on the stack + * Get the canvas and render into it + * Snap off and hold on to a GrDeferredDisplayList + * Once your app actually needs the pixels, call skgpu::ganesh::DrawDDL(GrDeferredDisplayList*) + * + * This class never accesses the GPU but performs all the cpu work it can. It + * is thread-safe (i.e., one can break a scene into tiles and perform their cpu-side + * work in parallel ahead of time). + */ +class SK_API GrDeferredDisplayListRecorder { +public: + GrDeferredDisplayListRecorder(const GrSurfaceCharacterization&); + ~GrDeferredDisplayListRecorder(); + + const GrSurfaceCharacterization& characterization() const { + return fCharacterization; + } + + // The backing canvas will become invalid (and this entry point will return + // null) once 'detach' is called. + // Note: ownership of the SkCanvas is not transferred via this call. + SkCanvas* getCanvas(); + + sk_sp detach(); + +private: + GrDeferredDisplayListRecorder(const GrDeferredDisplayListRecorder&) = delete; + GrDeferredDisplayListRecorder& operator=(const GrDeferredDisplayListRecorder&) = delete; + + bool init(); + + const GrSurfaceCharacterization fCharacterization; + sk_sp fContext; + sk_sp fTargetProxy; + sk_sp fLazyProxyData; + sk_sp fSurface; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrPromiseImageTexture.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrPromiseImageTexture.h new file mode 100644 index 00000000000..0f144c92f12 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrPromiseImageTexture.h @@ -0,0 +1,43 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrPromiseImageTexture_DEFINED +#define GrPromiseImageTexture_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" +#include "include/gpu/GrBackendSurface.h" +/** + * This type is used to fulfill textures for PromiseImages. Once an instance is returned from a + * PromiseImageTextureFulfillProc the GrBackendTexture it wraps must remain valid until the + * corresponding PromiseImageTextureReleaseProc is called. + */ +class SK_API GrPromiseImageTexture : public SkNVRefCnt { +public: + GrPromiseImageTexture() = delete; + GrPromiseImageTexture(const GrPromiseImageTexture&) = delete; + GrPromiseImageTexture(GrPromiseImageTexture&&) = delete; + ~GrPromiseImageTexture(); + GrPromiseImageTexture& operator=(const GrPromiseImageTexture&) = delete; + GrPromiseImageTexture& operator=(GrPromiseImageTexture&&) = delete; + + static sk_sp Make(const GrBackendTexture& backendTexture) { + if (!backendTexture.isValid()) { + return nullptr; + } + return sk_sp(new GrPromiseImageTexture(backendTexture)); + } + + GrBackendTexture backendTexture() const { return fBackendTexture; } + +private: + explicit GrPromiseImageTexture(const GrBackendTexture& backendTexture); + + GrBackendTexture fBackendTexture; +}; + +#endif // GrPromiseImageTexture_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrSurfaceCharacterization.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrSurfaceCharacterization.h new file mode 100644 index 00000000000..ecf3f92f9d0 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrSurfaceCharacterization.h @@ -0,0 +1,211 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrSurfaceCharacterization_DEFINED +#define GrSurfaceCharacterization_DEFINED + +#include "include/core/SkColorSpace.h" // IWYU pragma: keep +#include "include/core/SkColorType.h" +#include "include/core/SkImageInfo.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkSize.h" +#include "include/core/SkSurfaceProps.h" +#include "include/core/SkTypes.h" +#include "include/gpu/GpuTypes.h" +#include "include/gpu/GrBackendSurface.h" +#include "include/gpu/GrContextThreadSafeProxy.h" +#include "include/gpu/GrTypes.h" +#include "include/private/base/SkDebug.h" + +#include +#include + +/** \class GrSurfaceCharacterization + A surface characterization contains all the information Ganesh requires to makes its internal + rendering decisions. When passed into a GrDeferredDisplayListRecorder it will copy the + data and pass it on to the GrDeferredDisplayList if/when it is created. Note that both of + those objects (the Recorder and the DisplayList) will take a ref on the + GrContextThreadSafeProxy and SkColorSpace objects. +*/ +class SK_API GrSurfaceCharacterization { +public: + enum class Textureable : bool { kNo = false, kYes = true }; + enum class UsesGLFBO0 : bool { kNo = false, kYes = true }; + // This flag indicates that the backing VkImage for this Vulkan surface will have the + // VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT set. This bit allows skia to handle advanced blends + // more optimally in a shader by being able to directly read the dst values. + enum class VkRTSupportsInputAttachment : bool { kNo = false, kYes = true }; + // This flag indicates if the surface is wrapping a raw Vulkan secondary command buffer. + enum class VulkanSecondaryCBCompatible : bool { kNo = false, kYes = true }; + + GrSurfaceCharacterization() + : fCacheMaxResourceBytes(0) + , fOrigin(kBottomLeft_GrSurfaceOrigin) + , fSampleCnt(0) + , fIsTextureable(Textureable::kYes) + , fIsMipmapped(skgpu::Mipmapped::kYes) + , fUsesGLFBO0(UsesGLFBO0::kNo) + , fVulkanSecondaryCBCompatible(VulkanSecondaryCBCompatible::kNo) + , fIsProtected(skgpu::Protected::kNo) + , fSurfaceProps() {} + + GrSurfaceCharacterization(GrSurfaceCharacterization&&) = default; + GrSurfaceCharacterization& operator=(GrSurfaceCharacterization&&) = default; + + GrSurfaceCharacterization(const GrSurfaceCharacterization&) = default; + GrSurfaceCharacterization& operator=(const GrSurfaceCharacterization& other) = default; + bool operator==(const GrSurfaceCharacterization& other) const; + bool operator!=(const GrSurfaceCharacterization& other) const { + return !(*this == other); + } + + /* + * Return a new surface characterization with the only difference being a different width + * and height + */ + GrSurfaceCharacterization createResized(int width, int height) const; + + /* + * Return a new surface characterization with only a replaced color space + */ + GrSurfaceCharacterization createColorSpace(sk_sp) const; + + /* + * Return a new surface characterization with the backend format replaced. A colorType + * must also be supplied to indicate the interpretation of the new format. + */ + GrSurfaceCharacterization createBackendFormat(SkColorType colorType, + const GrBackendFormat& backendFormat) const; + + /* + * Return a new surface characterization with just a different use of FBO0 (in GL) + */ + GrSurfaceCharacterization createFBO0(bool usesGLFBO0) const; + + GrContextThreadSafeProxy* contextInfo() const { return fContextInfo.get(); } + sk_sp refContextInfo() const { return fContextInfo; } + size_t cacheMaxResourceBytes() const { return fCacheMaxResourceBytes; } + + bool isValid() const { return kUnknown_SkColorType != fImageInfo.colorType(); } + + const SkImageInfo& imageInfo() const { return fImageInfo; } + const GrBackendFormat& backendFormat() const { return fBackendFormat; } + GrSurfaceOrigin origin() const { return fOrigin; } + SkISize dimensions() const { return fImageInfo.dimensions(); } + int width() const { return fImageInfo.width(); } + int height() const { return fImageInfo.height(); } + SkColorType colorType() const { return fImageInfo.colorType(); } + int sampleCount() const { return fSampleCnt; } + bool isTextureable() const { return Textureable::kYes == fIsTextureable; } + bool isMipMapped() const { return skgpu::Mipmapped::kYes == fIsMipmapped; } + bool usesGLFBO0() const { return UsesGLFBO0::kYes == fUsesGLFBO0; } + bool vkRTSupportsInputAttachment() const { + return VkRTSupportsInputAttachment::kYes == fVkRTSupportsInputAttachment; + } + bool vulkanSecondaryCBCompatible() const { + return VulkanSecondaryCBCompatible::kYes == fVulkanSecondaryCBCompatible; + } + skgpu::Protected isProtected() const { return fIsProtected; } + SkColorSpace* colorSpace() const { return fImageInfo.colorSpace(); } + sk_sp refColorSpace() const { return fImageInfo.refColorSpace(); } + const SkSurfaceProps& surfaceProps()const { return fSurfaceProps; } + +private: + friend class SkSurface_Ganesh; // for 'set' & 'config' + friend class GrVkSecondaryCBDrawContext; // for 'set' & 'config' + friend class GrContextThreadSafeProxy; // for private ctor + friend class GrVkContextThreadSafeProxy; // for private ctor + friend class GrDeferredDisplayListRecorder; // for 'config' + friend class SkSurface; // for 'config' + + SkDEBUGCODE(void validate() const;) + + GrSurfaceCharacterization(sk_sp contextInfo, + size_t cacheMaxResourceBytes, + const SkImageInfo& ii, + const GrBackendFormat& backendFormat, + GrSurfaceOrigin origin, + int sampleCnt, + Textureable isTextureable, + skgpu::Mipmapped isMipmapped, + UsesGLFBO0 usesGLFBO0, + VkRTSupportsInputAttachment vkRTSupportsInputAttachment, + VulkanSecondaryCBCompatible vulkanSecondaryCBCompatible, + skgpu::Protected isProtected, + const SkSurfaceProps& surfaceProps) + : fContextInfo(std::move(contextInfo)) + , fCacheMaxResourceBytes(cacheMaxResourceBytes) + , fImageInfo(ii) + , fBackendFormat(std::move(backendFormat)) + , fOrigin(origin) + , fSampleCnt(sampleCnt) + , fIsTextureable(isTextureable) + , fIsMipmapped(isMipmapped) + , fUsesGLFBO0(usesGLFBO0) + , fVkRTSupportsInputAttachment(vkRTSupportsInputAttachment) + , fVulkanSecondaryCBCompatible(vulkanSecondaryCBCompatible) + , fIsProtected(isProtected) + , fSurfaceProps(surfaceProps) { + if (fSurfaceProps.flags() & SkSurfaceProps::kDynamicMSAA_Flag) { + // Dynamic MSAA is not currently supported with DDL. + *this = {}; + } + SkDEBUGCODE(this->validate()); + } + + void set(sk_sp contextInfo, + size_t cacheMaxResourceBytes, + const SkImageInfo& ii, + const GrBackendFormat& backendFormat, + GrSurfaceOrigin origin, + int sampleCnt, + Textureable isTextureable, + skgpu::Mipmapped isMipmapped, + UsesGLFBO0 usesGLFBO0, + VkRTSupportsInputAttachment vkRTSupportsInputAttachment, + VulkanSecondaryCBCompatible vulkanSecondaryCBCompatible, + skgpu::Protected isProtected, + const SkSurfaceProps& surfaceProps) { + if (surfaceProps.flags() & SkSurfaceProps::kDynamicMSAA_Flag) { + // Dynamic MSAA is not currently supported with DDL. + *this = {}; + } else { + fContextInfo = std::move(contextInfo); + fCacheMaxResourceBytes = cacheMaxResourceBytes; + + fImageInfo = ii; + fBackendFormat = std::move(backendFormat); + fOrigin = origin; + fSampleCnt = sampleCnt; + fIsTextureable = isTextureable; + fIsMipmapped = isMipmapped; + fUsesGLFBO0 = usesGLFBO0; + fVkRTSupportsInputAttachment = vkRTSupportsInputAttachment; + fVulkanSecondaryCBCompatible = vulkanSecondaryCBCompatible; + fIsProtected = isProtected; + fSurfaceProps = surfaceProps; + } + SkDEBUGCODE(this->validate()); + } + + sk_sp fContextInfo; + size_t fCacheMaxResourceBytes; + + SkImageInfo fImageInfo; + GrBackendFormat fBackendFormat; + GrSurfaceOrigin fOrigin; + int fSampleCnt; + Textureable fIsTextureable; + skgpu::Mipmapped fIsMipmapped; + UsesGLFBO0 fUsesGLFBO0; + VkRTSupportsInputAttachment fVkRTSupportsInputAttachment; + VulkanSecondaryCBCompatible fVulkanSecondaryCBCompatible; + skgpu::Protected fIsProtected; + SkSurfaceProps fSurfaceProps; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrVkSecondaryCBDrawContext.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrVkSecondaryCBDrawContext.h new file mode 100644 index 00000000000..d0348312bd8 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/GrVkSecondaryCBDrawContext.h @@ -0,0 +1,131 @@ +/* + * Copyright 2019 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrVkSecondaryCBDrawContext_DEFINED +#define GrVkSecondaryCBDrawContext_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkSurfaceProps.h" +#include "include/core/SkTypes.h" + +#include + +class GrBackendSemaphore; +class GrDeferredDisplayList; +class GrRecordingContext; +class GrSurfaceCharacterization; +struct GrVkDrawableInfo; +namespace skgpu::ganesh { +class Device; +} +class SkCanvas; +struct SkImageInfo; +class SkSurfaceProps; + +/** + * This class is a private header that is intended to only be used inside of Chromium. This requires + * Chromium to burrow in and include this specifically since it is not part of skia's public include + * directory. + */ + +/** + * This class is used to draw into an external Vulkan secondary command buffer that is imported + * by the client. The secondary command buffer that gets imported must already have had begin called + * on it with VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT. Thus any draws to the imported + * command buffer cannot require changing the render pass. This requirement means that certain types + * of draws will not be supported when using a GrVkSecondaryCBDrawContext. This includes: + * Draws that require a dst copy for blending will be dropped + * Text draws will be dropped (these may require intermediate uploads of text data) + * Read and Write pixels will not work + * Any other draw that requires a copy will fail (this includes using backdrop filter with save + * layer). + * Stenciling is also disabled, but that should not restrict any actual draws from working. + * + * While using a GrVkSecondaryCBDrawContext, the client can also draw into normal SkSurfaces and + * then draw those SkSufaces (as SkImages) into the GrVkSecondaryCBDrawContext. If any of the + * previously mentioned unsupported draws are needed by the client, they can draw them into an + * offscreen surface, and then draw that into the GrVkSecondaryCBDrawContext. + * + * After all drawing to the GrVkSecondaryCBDrawContext has been done, the client must call flush() + * on the GrVkSecondaryCBDrawContext to actually fill in the secondary VkCommandBuffer with the + * draws. + * + * Additionally, the client must keep the GrVkSecondaryCBDrawContext alive until the secondary + * VkCommandBuffer has been submitted and all work finished on the GPU. Before deleting the + * GrVkSecondaryCBDrawContext, the client must call releaseResources() so that Skia can cleanup + * any internal objects that were created for the draws into the secondary command buffer. + */ +class SK_SPI GrVkSecondaryCBDrawContext : public SkRefCnt { +public: + static sk_sp Make(GrRecordingContext*, + const SkImageInfo&, + const GrVkDrawableInfo&, + const SkSurfaceProps* props); + + ~GrVkSecondaryCBDrawContext() override; + + SkCanvas* getCanvas(); + + // Records all the draws to the imported secondary command buffer and sets any dependent + // offscreen draws to the GPU. + void flush(); + + /** Inserts a list of GPU semaphores that Skia will have the driver wait on before executing + commands for this secondary CB. The wait semaphores will get added to the VkCommandBuffer + owned by this GrContext when flush() is called, and not the command buffer which the + Secondary CB is from. This will guarantee that the driver waits on the semaphores before + the secondary command buffer gets executed. We will submit the semphore to wait at + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT and VK_PIPELINE_STAGE_TRANSFER_BIT. If this + call returns false, then the GPU back end will not wait on any passed in semaphores, and the + client will still own the semaphores, regardless of the value of deleteSemaphoresAfterWait. + + If deleteSemaphoresAfterWait is false then Skia will not delete the semaphores. In this case + it is the client's responsibility to not destroy or attempt to reuse the semaphores until it + knows that Skia has finished waiting on them. This can be done by using finishedProcs + on flush calls. + + @param numSemaphores size of waitSemaphores array + @param waitSemaphores array of semaphore containers + @paramm deleteSemaphoresAfterWait who owns and should delete the semaphores + @return true if GPU is waiting on semaphores + */ + bool wait(int numSemaphores, + const GrBackendSemaphore waitSemaphores[], + bool deleteSemaphoresAfterWait = true); + + // This call will release all resources held by the draw context. The client must call + // releaseResources() before deleting the drawing context. However, the resources also include + // any Vulkan resources that were created and used for draws. Therefore the client must only + // call releaseResources() after submitting the secondary command buffer, and waiting for it to + // finish on the GPU. If it is called earlier then some vulkan objects may be deleted while they + // are still in use by the GPU. + void releaseResources(); + + const SkSurfaceProps& props() const { return fProps; } + + // TODO: Fill out these calls to support DDL + bool characterize(GrSurfaceCharacterization* characterization) const; + +#ifndef SK_DDL_IS_UNIQUE_POINTER + bool draw(sk_sp deferredDisplayList); +#else + bool draw(const GrDeferredDisplayList* deferredDisplayList); +#endif + + bool isCompatible(const GrSurfaceCharacterization& characterization) const; + +private: + explicit GrVkSecondaryCBDrawContext(sk_sp, const SkSurfaceProps*); + + sk_sp fDevice; + std::unique_ptr fCachedCanvas; + const SkSurfaceProps fProps; + + using INHERITED = SkRefCnt; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/SkChromeRemoteGlyphCache.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/SkChromeRemoteGlyphCache.h new file mode 100644 index 00000000000..3a71a45e174 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/SkChromeRemoteGlyphCache.h @@ -0,0 +1,150 @@ +/* + * Copyright 2021 Google LLC. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkChromeRemoteGlyphCache_DEFINED +#define SkChromeRemoteGlyphCache_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypeface.h" +#include "include/private/base/SkAPI.h" + +#include +#include +#include +#include + +class SkAutoDescriptor; +class SkCanvas; +class SkColorSpace; +class SkStrikeCache; +class SkStrikeClientImpl; +class SkStrikeServerImpl; +class SkSurfaceProps; +namespace sktext::gpu { class Slug; } + +using SkDiscardableHandleId = uint32_t; +// This class is not thread-safe. +class SkStrikeServer { +public: + // An interface used by the server to create handles for pinning SkStrike + // entries on the remote client. + class DiscardableHandleManager { + public: + SK_SPI virtual ~DiscardableHandleManager() = default; + + // Creates a new *locked* handle and returns a unique ID that can be used to identify + // it on the remote client. + SK_SPI virtual SkDiscardableHandleId createHandle() = 0; + + // Returns true if the handle could be successfully locked. The server can + // assume it will remain locked until the next set of serialized entries is + // pulled from the SkStrikeServer. + // If returns false, the cache entry mapped to the handle has been deleted + // on the client. Any subsequent attempts to lock the same handle are not + // allowed. + SK_SPI virtual bool lockHandle(SkDiscardableHandleId) = 0; + + // Returns true if a handle has been deleted on the remote client. It is + // invalid to use a handle id again with this manager once this returns true. + SK_SPI virtual bool isHandleDeleted(SkDiscardableHandleId) = 0; + }; + + SK_SPI explicit SkStrikeServer(DiscardableHandleManager* discardableHandleManager); + SK_SPI ~SkStrikeServer(); + + // Create an analysis SkCanvas used to populate the SkStrikeServer with ops + // which will be serialized and rendered using the SkStrikeClient. + SK_API std::unique_ptr makeAnalysisCanvas(int width, int height, + const SkSurfaceProps& props, + sk_sp colorSpace, + bool DFTSupport, + bool DFTPerspSupport = true); + + // Serializes the strike data captured using a canvas returned by ::makeAnalysisCanvas. Any + // handles locked using the DiscardableHandleManager will be assumed to be + // unlocked after this call. + SK_SPI void writeStrikeData(std::vector* memory); + + // Testing helpers + void setMaxEntriesInDescriptorMapForTesting(size_t count); + size_t remoteStrikeMapSizeForTesting() const; + +private: + SkStrikeServerImpl* impl(); + + std::unique_ptr fImpl; +}; + +class SkStrikeClient { +public: + // This enum is used in histogram reporting in chromium. Please don't re-order the list of + // entries, and consider it to be append-only. + enum CacheMissType : uint32_t { + // Hard failures where no fallback could be found. + kFontMetrics = 0, + kGlyphMetrics = 1, + kGlyphImage = 2, + kGlyphPath = 3, + + // (DEPRECATED) The original glyph could not be found and a fallback was used. + kGlyphMetricsFallback = 4, + kGlyphPathFallback = 5, + + kGlyphDrawable = 6, + kLast = kGlyphDrawable + }; + + // An interface to delete handles that may be pinned by the remote server. + class DiscardableHandleManager : public SkRefCnt { + public: + ~DiscardableHandleManager() override = default; + + // Returns true if the handle was unlocked and can be safely deleted. Once + // successful, subsequent attempts to delete the same handle are invalid. + virtual bool deleteHandle(SkDiscardableHandleId) = 0; + + virtual void assertHandleValid(SkDiscardableHandleId) {} + + virtual void notifyCacheMiss(CacheMissType type, int fontSize) = 0; + + struct ReadFailureData { + size_t memorySize; + size_t bytesRead; + uint64_t typefaceSize; + uint64_t strikeCount; + uint64_t glyphImagesCount; + uint64_t glyphPathsCount; + }; + virtual void notifyReadFailure(const ReadFailureData& data) {} + }; + + SK_SPI explicit SkStrikeClient(sk_sp, + bool isLogging = true, + SkStrikeCache* strikeCache = nullptr); + SK_SPI ~SkStrikeClient(); + + // Deserializes the strike data from a SkStrikeServer. All messages generated + // from a server when serializing the ops must be deserialized before the op + // is rasterized. + // Returns false if the data is invalid. + SK_SPI bool readStrikeData(const volatile void* memory, size_t memorySize); + + // Given a descriptor re-write the Rec mapping the typefaceID from the renderer to the + // corresponding typefaceID on the GPU. + SK_SPI bool translateTypefaceID(SkAutoDescriptor* descriptor) const; + + // Testing helpers + sk_sp retrieveTypefaceUsingServerIDForTest(SkTypefaceID) const; + + // Given a buffer, unflatten into a slug making sure to do the typefaceID translation from + // renderer to GPU. Returns nullptr if there was a problem. + sk_sp deserializeSlugForTest(const void* data, size_t size) const; + +private: + std::unique_ptr fImpl; +}; +#endif // SkChromeRemoteGlyphCache_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/SkDiscardableMemory.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/SkDiscardableMemory.h new file mode 100644 index 00000000000..3aa98703605 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/SkDiscardableMemory.h @@ -0,0 +1,70 @@ +/* + * Copyright 2013 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkDiscardableMemory_DEFINED +#define SkDiscardableMemory_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" + +/** + * Interface for discardable memory. Implementation is provided by the + * embedder. + */ +class SK_SPI SkDiscardableMemory { +public: + /** + * Factory method that creates, initializes and locks an SkDiscardableMemory + * object. If either of these steps fails, a nullptr pointer will be returned. + */ + static SkDiscardableMemory* Create(size_t bytes); + + /** + * Factory class that creates, initializes and locks an SkDiscardableMemory + * object. If either of these steps fails, a nullptr pointer will be returned. + */ + class Factory : public SkRefCnt { + public: + virtual SkDiscardableMemory* create(size_t bytes) = 0; + private: + using INHERITED = SkRefCnt; + }; + + /** Must not be called while locked. + */ + virtual ~SkDiscardableMemory() {} + + /** + * Locks the memory, prevent it from being discarded. Once locked. you may + * obtain a pointer to that memory using the data() method. + * + * lock() may return false, indicating that the underlying memory was + * discarded and that the lock failed. + * + * Nested calls to lock are not allowed. + */ + [[nodiscard]] virtual bool lock() = 0; + + /** + * Returns the current pointer for the discardable memory. This call is ONLY + * valid when the discardable memory object is locked. + */ + virtual void* data() = 0; + + /** + * Unlock the memory so that it can be purged by the system. Must be called + * after every successful lock call. + */ + virtual void unlock() = 0; + +protected: + SkDiscardableMemory() = default; + SkDiscardableMemory(const SkDiscardableMemory&) = delete; + SkDiscardableMemory& operator=(const SkDiscardableMemory&) = delete; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/SkImageChromium.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/SkImageChromium.h new file mode 100644 index 00000000000..7c62ba581b6 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/SkImageChromium.h @@ -0,0 +1,117 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkImageChromium_DEFINED +#define SkImageChromium_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +class GrBackendFormat; +class GrContextThreadSafeProxy; +class GrPromiseImageTexture; +class GrDirectContext; +class GrYUVABackendTextureInfo; +class SkColorSpace; +class SkImage; +enum SkAlphaType : int; +enum SkColorType : int; +enum GrSurfaceOrigin : int; +namespace skgpu { +enum class Mipmapped : bool; +} +struct SkISize; + +/** + * These functions expose features that are only for external use in Chromium. + */ + +namespace SkImages { + +using PromiseImageTextureContext = void*; +using PromiseImageTextureFulfillProc = sk_sp (*)(PromiseImageTextureContext); +using PromiseImageTextureReleaseProc = void (*)(PromiseImageTextureContext); + +/** Create a new GPU-backed SkImage that is very similar to an SkImage created by BorrowTextureFrom. + The difference is that the caller need not have created the texture nor populated it with the + image pixel data. Moreover, the SkImage may be created on a thread as the creation of the + image does not require access to the backend API or GrDirectContext. Instead of passing a + GrBackendTexture the client supplies a description of the texture consisting of + GrBackendFormat, width, height, and skgpu::Mipmapped state. The resulting SkImage can be drawn + to a GrDeferredDisplayListRecorder or directly to a GPU-backed SkSurface. + When the actual texture is required to perform a backend API draw, textureFulfillProc will + be called to receive a GrBackendTexture. The properties of the GrBackendTexture must match + those set during the SkImage creation, and it must refer to a valid existing texture in the + backend API context/device, and be populated with the image pixel data. The texture cannot + be deleted until textureReleaseProc is called. + There is at most one call to each of textureFulfillProc and textureReleaseProc. + textureReleaseProc is always called even if image creation fails or if the + image is never fulfilled (e.g. it is never drawn or all draws are clipped out) + @param gpuContextProxy the thread-safe proxy of the gpu context. required. + @param backendFormat format of promised gpu texture + @param dimensions width & height of promised gpu texture + @param mipmapped mip mapped state of promised gpu texture + @param origin surface origin of promised gpu texture + @param colorType color type of promised gpu texture + @param alphaType alpha type of promised gpu texture + @param colorSpace range of colors; may be nullptr + @param textureFulfillProc function called to get actual gpu texture + @param textureReleaseProc function called when texture can be deleted + @param textureContext state passed to textureFulfillProc and textureReleaseProc + @return created SkImage, or nullptr +*/ +SK_API sk_sp PromiseTextureFrom(sk_sp gpuContextProxy, + const GrBackendFormat& backendFormat, + SkISize dimensions, + skgpu::Mipmapped mipmapped, + GrSurfaceOrigin origin, + SkColorType colorType, + SkAlphaType alphaType, + sk_sp colorSpace, + PromiseImageTextureFulfillProc textureFulfillProc, + PromiseImageTextureReleaseProc textureReleaseProc, + PromiseImageTextureContext textureContext); + +/** This is similar to 'PromiseTextureFrom' but it creates a GPU-backed SkImage from YUV[A] data. + The source data may be planar (i.e. spread across multiple textures). In + the extreme Y, U, V, and A are all in different planes and thus the image is specified by + four textures. 'backendTextureInfo' describes the planar arrangement, texture formats, + conversion to RGB, and origin of the textures. Separate 'textureFulfillProc' and + 'textureReleaseProc' calls are made for each texture. Each texture has its own + PromiseImageTextureContext. If 'backendTextureInfo' is not valid then no release proc + calls are made. Otherwise, the calls will be made even on failure. 'textureContexts' has one + entry for each of the up to four textures, as indicated by 'backendTextureInfo'. + Currently the mip mapped property of 'backendTextureInfo' is ignored. However, in the + near future it will be required that if it is kYes then textureFulfillProc must return + a mip mapped texture for each plane in order to successfully draw the image. + @param gpuContextProxy the thread-safe proxy of the gpu context. required. + @param backendTextureInfo info about the promised yuva gpu texture + @param imageColorSpace range of colors; may be nullptr + @param textureFulfillProc function called to get actual gpu texture + @param textureReleaseProc function called when texture can be deleted + @param textureContexts state passed to textureFulfillProc and textureReleaseProc + @return created SkImage, or nullptr +*/ +SK_API sk_sp PromiseTextureFromYUVA(sk_sp gpuContextProxy, + const GrYUVABackendTextureInfo& backendTextureInfo, + sk_sp imageColorSpace, + PromiseImageTextureFulfillProc textureFulfillProc, + PromiseImageTextureReleaseProc textureReleaseProc, + PromiseImageTextureContext textureContexts[]); + +/** Returns the GPU context associated with this image or nullptr if the image is not Ganesh-backed. + We expose this only to help transition certain API calls and do not intend for this to stick + around forever. +*/ +SK_API GrDirectContext* GetContext(const SkImage* src); +inline GrDirectContext* GetContext(const sk_sp& src) { + return GetContext(src.get()); +} + +} // namespace SkImages + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/Slug.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/Slug.h new file mode 100644 index 00000000000..0ee6082b8a8 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/chromium/Slug.h @@ -0,0 +1,72 @@ +/* + * Copyright 2021 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef sktext_gpu_Slug_DEFINED +#define sktext_gpu_Slug_DEFINED + +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/private/base/SkAPI.h" + +#include +#include + +class SkCanvas; +class SkData; +class SkPaint; +class SkReadBuffer; +class SkStrikeClient; +class SkTextBlob; +class SkWriteBuffer; +struct SkDeserialProcs; +struct SkPoint; + +namespace sktext::gpu { +// Slug encapsulates an SkTextBlob at a specific origin, using a specific paint. It can be +// manipulated using matrix and clip changes to the canvas. If the canvas is transformed, then +// the Slug will also transform with smaller glyphs using bi-linear interpolation to render. You +// can think of a Slug as making a rubber stamp out of a SkTextBlob. +class SK_API Slug : public SkRefCnt { +public: + // Return nullptr if the blob would not draw. This is not because of clipping, but because of + // some paint optimization. The Slug is captured as if drawn using drawTextBlob. + static sk_sp ConvertBlob( + SkCanvas* canvas, const SkTextBlob& blob, SkPoint origin, const SkPaint& paint); + + // Serialize the slug. + sk_sp serialize() const; + size_t serialize(void* buffer, size_t size) const; + + // Set the client parameter to the appropriate SkStrikeClient when typeface ID translation + // is needed. + static sk_sp Deserialize(const void* data, + size_t size, + const SkStrikeClient* client = nullptr); + static sk_sp MakeFromBuffer(SkReadBuffer& buffer); + + // Allows clients to deserialize SkPictures that contain slug data + static void AddDeserialProcs(SkDeserialProcs* procs, const SkStrikeClient* client = nullptr); + + // Draw the Slug obeying the canvas's mapping and clipping. + void draw(SkCanvas* canvas, const SkPaint& paint) const; + + virtual SkRect sourceBounds() const = 0; + virtual SkRect sourceBoundsWithOrigin () const = 0; + + virtual void doFlatten(SkWriteBuffer&) const = 0; + + uint32_t uniqueID() const { return fUniqueID; } + +private: + static uint32_t NextUniqueID(); + const uint32_t fUniqueID{NextUniqueID()}; +}; + + +} // namespace sktext::gpu + +#endif // sktext_gpu_Slug_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrContext_Base.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrContext_Base.h new file mode 100644 index 00000000000..450bea411b4 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrContext_Base.h @@ -0,0 +1,104 @@ +/* + * Copyright 2019 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrContext_Base_DEFINED +#define GrContext_Base_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/gpu/GrTypes.h" +#include "include/private/base/SkAPI.h" + +#include + +class GrBaseContextPriv; +class GrCaps; +class GrContextThreadSafeProxy; +class GrDirectContext; +class GrImageContext; +class GrRecordingContext; +enum SkColorType : int; +enum class SkTextureCompressionType; +struct GrContextOptions; +class GrBackendFormat; + +class GrContext_Base : public SkRefCnt { +public: + ~GrContext_Base() override; + + /* + * Safely downcast to a GrDirectContext. + */ + virtual GrDirectContext* asDirectContext() { return nullptr; } + + /* + * The 3D API backing this context + */ + SK_API GrBackendApi backend() const; + + /* + * Retrieve the default GrBackendFormat for a given SkColorType and renderability. + * It is guaranteed that this backend format will be the one used by the GrContext + * SkColorType and GrSurfaceCharacterization-based createBackendTexture methods. + * + * The caller should check that the returned format is valid. + */ + SK_API GrBackendFormat defaultBackendFormat(SkColorType, GrRenderable) const; + + SK_API GrBackendFormat compressedBackendFormat(SkTextureCompressionType) const; + + /** + * Gets the maximum supported sample count for a color type. 1 is returned if only non-MSAA + * rendering is supported for the color type. 0 is returned if rendering to this color type + * is not supported at all. + */ + SK_API int maxSurfaceSampleCountForColorType(SkColorType colorType) const; + + // TODO: When the public version is gone, rename to refThreadSafeProxy and add raw ptr ver. + sk_sp threadSafeProxy(); + + // Provides access to functions that aren't part of the public API. + GrBaseContextPriv priv(); + const GrBaseContextPriv priv() const; // NOLINT(readability-const-return-type) + +protected: + friend class GrBaseContextPriv; // for hidden functions + + GrContext_Base(sk_sp); + + virtual bool init(); + + /** + * An identifier for this context. The id is used by all compatible contexts. For example, + * if SkImages are created on one thread using an image creation context, then fed into a + * DDL Recorder on second thread (which has a recording context) and finally replayed on + * a third thread with a direct context, then all three contexts will report the same id. + * It is an error for an image to be used with contexts that report different ids. + */ + uint32_t contextID() const; + + bool matches(GrContext_Base* candidate) const { + return candidate && candidate->contextID() == this->contextID(); + } + + /* + * The options in effect for this context + */ + const GrContextOptions& options() const; + + const GrCaps* caps() const; + sk_sp refCaps() const; + + virtual GrImageContext* asImageContext() { return nullptr; } + virtual GrRecordingContext* asRecordingContext() { return nullptr; } + + sk_sp fThreadSafeProxy; + +private: + using INHERITED = SkRefCnt; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrD3DTypesMinimal.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrD3DTypesMinimal.h new file mode 100644 index 00000000000..9d6156d6218 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrD3DTypesMinimal.h @@ -0,0 +1,74 @@ +/* + * Copyright 2020 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrD3DTypesMinimal_DEFINED +#define GrD3DTypesMinimal_DEFINED + +// Minimal definitions of Direct3D types, without including d3d12.h + +#include "include/core/SkRefCnt.h" + +#include + +#include "include/gpu/GrTypes.h" + +struct ID3D12Resource; +class GrD3DResourceState; +typedef int GrD3DResourceStateEnum; +struct GrD3DSurfaceInfo; +struct GrD3DTextureResourceInfo; +struct GrD3DTextureResourceSpec; +struct GrD3DFenceInfo; + +// This struct is to used to store the the actual information about the Direct3D backend image on +// GrBackendTexture and GrBackendRenderTarget. When a client calls getD3DTextureInfo on a +// GrBackendTexture/RenderTarget, we use the GrD3DBackendSurfaceInfo to create a snapshot +// GrD3DTextureResourceInfo object. Internally, this uses a ref count GrD3DResourceState object to +// track the current D3D12_RESOURCE_STATES which can be shared with an internal GrD3DTextureResource +// so that state updates can be seen by all users of the texture. +struct GrD3DBackendSurfaceInfo { + GrD3DBackendSurfaceInfo(const GrD3DTextureResourceInfo& info, GrD3DResourceState* state); + + void cleanup(); + + GrD3DBackendSurfaceInfo& operator=(const GrD3DBackendSurfaceInfo&) = delete; + + // Assigns the passed in GrD3DBackendSurfaceInfo to this object. if isValid is true we will also + // attempt to unref the old fLayout on this object. + void assign(const GrD3DBackendSurfaceInfo&, bool isValid); + + void setResourceState(GrD3DResourceStateEnum state); + + sk_sp getGrD3DResourceState() const; + + GrD3DTextureResourceInfo snapTextureResourceInfo() const; + + bool isProtected() const; +#if defined(GR_TEST_UTILS) + bool operator==(const GrD3DBackendSurfaceInfo& that) const; +#endif + +private: + GrD3DTextureResourceInfo* fTextureResourceInfo; + GrD3DResourceState* fResourceState; +}; + +struct GrD3DTextureResourceSpecHolder { +public: + GrD3DTextureResourceSpecHolder(const GrD3DSurfaceInfo&); + + void cleanup(); + + GrD3DSurfaceInfo getSurfaceInfo(uint32_t sampleCount, + uint32_t levelCount, + skgpu::Protected isProtected) const; + +private: + GrD3DTextureResourceSpec* fSpec; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrImageContext.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrImageContext.h new file mode 100644 index 00000000000..a8c81e2d222 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrImageContext.h @@ -0,0 +1,56 @@ +/* + * Copyright 2019 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrImageContext_DEFINED +#define GrImageContext_DEFINED + +#include "include/core/SkRefCnt.h" +#include "include/private/base/SingleOwner.h" +#include "include/private/base/SkAPI.h" +#include "include/private/gpu/ganesh/GrContext_Base.h" + +class GrContextThreadSafeProxy; +class GrImageContextPriv; + +// This is now just a view on a ThreadSafeProxy, that SkImages can attempt to +// downcast to a GrDirectContext as a backdoor to some operations. Once we remove the backdoors, +// this goes away and SkImages just hold ThreadSafeProxies. +class GrImageContext : public GrContext_Base { +public: + ~GrImageContext() override; + + // Provides access to functions that aren't part of the public API. + GrImageContextPriv priv(); + const GrImageContextPriv priv() const; // NOLINT(readability-const-return-type) + +protected: + friend class GrImageContextPriv; // for hidden functions + + GrImageContext(sk_sp); + + SK_API virtual void abandonContext(); + SK_API virtual bool abandoned(); + + /** This is only useful for debug purposes */ + skgpu::SingleOwner* singleOwner() const { return &fSingleOwner; } + + GrImageContext* asImageContext() override { return this; } + +private: + // When making promise images, we currently need a placeholder GrImageContext instance to give + // to the SkImage that has no real power, just a wrapper around the ThreadSafeProxy. + // TODO: De-power SkImage to ThreadSafeProxy or at least figure out a way to share one instance. + static sk_sp MakeForPromiseImage(sk_sp); + + // In debug builds we guard against improper thread handling + // This guard is passed to the GrDrawingManager and, from there to all the + // GrSurfaceDrawContexts. It is also passed to the GrResourceProvider and SkGpuDevice. + // TODO: Move this down to GrRecordingContext. + mutable skgpu::SingleOwner fSingleOwner; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrTextureGenerator.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrTextureGenerator.h new file mode 100644 index 00000000000..a8902d863e3 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrTextureGenerator.h @@ -0,0 +1,66 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrTextureGenerator_DEFINED +#define GrTextureGenerator_DEFINED + +#include "include/core/SkImageGenerator.h" +#include "include/gpu/GrTypes.h" +#include "include/private/base/SkAPI.h" + +#include + +class GrRecordingContext; +class GrSurfaceProxyView; +enum class GrImageTexGenPolicy : int; +namespace skgpu { enum class Mipmapped : bool; } +struct SkImageInfo; + +class SK_API GrTextureGenerator : public SkImageGenerator { +public: + bool isTextureGenerator() const final { return true; } + + /** + * If the generator can natively/efficiently return its pixels as a GPU image (backed by a + * texture) this will return that image. If not, this will return NULL. + * + * Regarding the GrRecordingContext parameter: + * + * It must be non-NULL. The generator should only succeed if: + * - its internal context is the same + * - it can somehow convert its texture into one that is valid for the provided context. + * + * If the mipmapped parameter is kYes, the generator should try to create a TextureProxy that + * at least has the mip levels allocated and the base layer filled in. If this is not possible, + * the generator is allowed to return a non mipped proxy, but this will have some additional + * overhead in later allocating mips and copying of the base layer. + * + * GrImageTexGenPolicy determines whether or not a new texture must be created (and its budget + * status) or whether this may (but is not required to) return a pre-existing texture that is + * retained by the generator (kDraw). + */ + GrSurfaceProxyView generateTexture(GrRecordingContext*, + const SkImageInfo& info, + skgpu::Mipmapped mipmapped, + GrImageTexGenPolicy); + + // External clients should override GrExternalTextureGenerator instead of trying to implement + // this (which uses private Skia types) + virtual GrSurfaceProxyView onGenerateTexture(GrRecordingContext*, const SkImageInfo&, + skgpu::Mipmapped, GrImageTexGenPolicy) = 0; + + // Most internal SkImageGenerators produce textures and views that use kTopLeft_GrSurfaceOrigin. + // If the generator may produce textures with different origins (e.g. + // GrAHardwareBufferImageGenerator) it should override this function to return the correct + // origin. Implementations should be thread-safe. + virtual GrSurfaceOrigin origin() const { return kTopLeft_GrSurfaceOrigin; } + +protected: + GrTextureGenerator(const SkImageInfo& info, uint32_t uniqueId = kNeedNewImageUniqueID); +}; + +#endif // GrTextureGenerator_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrTypesPriv.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrTypesPriv.h new file mode 100644 index 00000000000..5568cb54de2 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/ganesh/GrTypesPriv.h @@ -0,0 +1,1007 @@ +/* + * Copyright 2013 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrTypesPriv_DEFINED +#define GrTypesPriv_DEFINED + +#include "include/core/SkColor.h" +#include "include/core/SkColorType.h" +#include "include/core/SkData.h" +#include "include/core/SkPath.h" +#include "include/core/SkPathTypes.h" +#include "include/core/SkRefCnt.h" +#include "include/gpu/GrTypes.h" +#include "include/private/base/SkAssert.h" +#include "include/private/base/SkDebug.h" +#include "include/private/base/SkMacros.h" +#include "include/private/base/SkTypeTraits.h" + +#include +#include +#include +#include + +class GrSurfaceProxy; + +namespace skgpu { +enum class Mipmapped : bool; +} + +/** + * divide, rounding up + */ + +static inline constexpr size_t GrSizeDivRoundUp(size_t x, size_t y) { return (x + (y - 1)) / y; } + +/** + * Geometric primitives used for drawing. + */ +enum class GrPrimitiveType : uint8_t { + kTriangles, + kTriangleStrip, + kPoints, + kLines, // 1 pix wide only + kLineStrip, // 1 pix wide only +}; +static constexpr int kNumGrPrimitiveTypes = (int)GrPrimitiveType::kLineStrip + 1; + +static constexpr bool GrIsPrimTypeLines(GrPrimitiveType type) { + return GrPrimitiveType::kLines == type || GrPrimitiveType::kLineStrip == type; +} + +enum class GrPrimitiveRestart : bool { + kNo = false, + kYes = true +}; + +/** + * Should a created surface be texturable? + */ +enum class GrTexturable : bool { + kNo = false, + kYes = true +}; + +// A DDL recorder has its own proxy provider and proxy cache. This enum indicates if +// a given proxy provider is one of these special ones. +enum class GrDDLProvider : bool { + kNo = false, + kYes = true +}; + +/** Ownership rules for external GPU resources imported into Skia. */ +enum GrWrapOwnership { + /** Skia will assume the client will keep the resource alive and Skia will not free it. */ + kBorrow_GrWrapOwnership, + + /** Skia will assume ownership of the resource and free it. */ + kAdopt_GrWrapOwnership, +}; + +enum class GrWrapCacheable : bool { + /** + * The wrapped resource will be removed from the cache as soon as it becomes purgeable. It may + * still be assigned and found by a unique key, but the presence of the key will not be used to + * keep the resource alive when it has no references. + */ + kNo = false, + /** + * The wrapped resource is allowed to remain in the GrResourceCache when it has no references + * but has a unique key. Such resources should only be given unique keys when it is known that + * the key will eventually be removed from the resource or invalidated via the message bus. + */ + kYes = true +}; + +enum class GrBudgetedType : uint8_t { + /** The resource is budgeted and is subject to purging under budget pressure. */ + kBudgeted, + /** + * The resource is unbudgeted and is purged as soon as it has no refs regardless of whether + * it has a unique or scratch key. + */ + kUnbudgetedUncacheable, + /** + * The resource is unbudgeted and is allowed to remain in the cache with no refs if it + * has a unique key. Scratch keys are ignored. + */ + kUnbudgetedCacheable, +}; + +enum class GrScissorTest : bool { + kDisabled = false, + kEnabled = true +}; + +/* + * Used to say whether texture is backed by memory. + */ +enum class GrMemoryless : bool { + /** + * The texture will be allocated normally and will affect memory budgets. + */ + kNo = false, + /** + * The texture will be not use GPU memory and will not affect memory budgets. + */ + kYes = true +}; + +struct GrMipLevel { + const void* fPixels = nullptr; + size_t fRowBytes = 0; + // This may be used to keep fPixels from being freed while a GrMipLevel exists. + sk_sp fOptionalStorage; + + static_assert(::sk_is_trivially_relocatable::value); + static_assert(::sk_is_trivially_relocatable::value); + + using sk_is_trivially_relocatable = std::true_type; +}; + +enum class GrSemaphoreWrapType { + kWillSignal, + kWillWait, +}; + +/** + * This enum is used to specify the load operation to be used when an OpsTask/GrOpsRenderPass + * begins execution. + */ +enum class GrLoadOp { + kLoad, + kClear, + kDiscard, +}; + +/** + * This enum is used to specify the store operation to be used when an OpsTask/GrOpsRenderPass + * ends execution. + */ +enum class GrStoreOp { + kStore, + kDiscard, +}; + +/** + * Used to control antialiasing in draw calls. + */ +enum class GrAA : bool { + kNo = false, + kYes = true +}; + +enum class GrFillRule : bool { + kNonzero, + kEvenOdd +}; + +inline GrFillRule GrFillRuleForPathFillType(SkPathFillType fillType) { + switch (fillType) { + case SkPathFillType::kWinding: + case SkPathFillType::kInverseWinding: + return GrFillRule::kNonzero; + case SkPathFillType::kEvenOdd: + case SkPathFillType::kInverseEvenOdd: + return GrFillRule::kEvenOdd; + } + SkUNREACHABLE; +} + +inline GrFillRule GrFillRuleForSkPath(const SkPath& path) { + return GrFillRuleForPathFillType(path.getFillType()); +} + +/** This enum indicates the type of antialiasing to be performed. */ +enum class GrAAType : unsigned { + /** No antialiasing */ + kNone, + /** Use fragment shader code to blend with a fractional pixel coverage. */ + kCoverage, + /** Use normal MSAA. */ + kMSAA, + + kLast = kMSAA +}; +static const int kGrAATypeCount = static_cast(GrAAType::kLast) + 1; + +static constexpr bool GrAATypeIsHW(GrAAType type) { + switch (type) { + case GrAAType::kNone: + return false; + case GrAAType::kCoverage: + return false; + case GrAAType::kMSAA: + return true; + } + SkUNREACHABLE; +} + +/** + * Some pixel configs are inherently clamped to [0,1], some are allowed to go outside that range, + * and some are FP but manually clamped in the XP. + */ +enum class GrClampType { + kAuto, // Normalized, fixed-point configs + kManual, // Clamped FP configs + kNone, // Normal (unclamped) FP configs +}; + +/** + * A number of rectangle/quadrilateral drawing APIs can control anti-aliasing on a per edge basis. + * These masks specify which edges are AA'ed. The intent for this is to support tiling with seamless + * boundaries, where the inner edges are non-AA and the outer edges are AA. Regular rectangle draws + * simply use kAll or kNone depending on if they want anti-aliasing or not. + * + * In APIs that support per-edge AA, GrQuadAAFlags is the only AA-control parameter that is + * provided (compared to the typical GrAA parameter). kNone is equivalent to GrAA::kNo, and any + * other set of edge flags would require GrAA::kYes (with rendering output dependent on how that + * maps to GrAAType for a given SurfaceDrawContext). + * + * These values are identical to SkCanvas::QuadAAFlags. + */ +enum class GrQuadAAFlags { + kLeft = 0b0001, + kTop = 0b0010, + kRight = 0b0100, + kBottom = 0b1000, + + kNone = 0b0000, + kAll = 0b1111, +}; + +GR_MAKE_BITFIELD_CLASS_OPS(GrQuadAAFlags) + +static inline GrQuadAAFlags SkToGrQuadAAFlags(unsigned flags) { + return static_cast(flags); +} + +/** + * The type of texture. Backends other than GL currently only use the 2D value but the type must + * still be known at the API-neutral layer as it used to determine whether MIP maps, renderability, + * and sampling parameters are legal for proxies that will be instantiated with wrapped textures. + */ +enum class GrTextureType { + kNone, + k2D, + /* Rectangle uses unnormalized texture coordinates. */ + kRectangle, + kExternal +}; + +enum GrShaderType { + kVertex_GrShaderType, + kFragment_GrShaderType, + + kLast_GrShaderType = kFragment_GrShaderType +}; +static const int kGrShaderTypeCount = kLast_GrShaderType + 1; + +enum GrShaderFlags { + kNone_GrShaderFlags = 0, + kVertex_GrShaderFlag = 1 << 0, + kFragment_GrShaderFlag = 1 << 1 +}; +SK_MAKE_BITFIELD_OPS(GrShaderFlags) + +/** Rectangle and external textures only support the clamp wrap mode and do not support + * MIP maps. + */ +static inline bool GrTextureTypeHasRestrictedSampling(GrTextureType type) { + switch (type) { + case GrTextureType::k2D: + return false; + case GrTextureType::kRectangle: + return true; + case GrTextureType::kExternal: + return true; + default: + SK_ABORT("Unexpected texture type"); + } +} + +////////////////////////////////////////////////////////////////////////////// + +/** + * Types used to describe format of vertices in arrays. + */ +enum GrVertexAttribType { + kFloat_GrVertexAttribType = 0, + kFloat2_GrVertexAttribType, + kFloat3_GrVertexAttribType, + kFloat4_GrVertexAttribType, + kHalf_GrVertexAttribType, + kHalf2_GrVertexAttribType, + kHalf4_GrVertexAttribType, + + kInt2_GrVertexAttribType, // vector of 2 32-bit ints + kInt3_GrVertexAttribType, // vector of 3 32-bit ints + kInt4_GrVertexAttribType, // vector of 4 32-bit ints + + + kByte_GrVertexAttribType, // signed byte + kByte2_GrVertexAttribType, // vector of 2 8-bit signed bytes + kByte4_GrVertexAttribType, // vector of 4 8-bit signed bytes + kUByte_GrVertexAttribType, // unsigned byte + kUByte2_GrVertexAttribType, // vector of 2 8-bit unsigned bytes + kUByte4_GrVertexAttribType, // vector of 4 8-bit unsigned bytes + + kUByte_norm_GrVertexAttribType, // unsigned byte, e.g. coverage, 0 -> 0.0f, 255 -> 1.0f. + kUByte4_norm_GrVertexAttribType, // vector of 4 unsigned bytes, e.g. colors, 0 -> 0.0f, + // 255 -> 1.0f. + + kShort2_GrVertexAttribType, // vector of 2 16-bit shorts. + kShort4_GrVertexAttribType, // vector of 4 16-bit shorts. + + kUShort2_GrVertexAttribType, // vector of 2 unsigned shorts. 0 -> 0, 65535 -> 65535. + kUShort2_norm_GrVertexAttribType, // vector of 2 unsigned shorts. 0 -> 0.0f, 65535 -> 1.0f. + + kInt_GrVertexAttribType, + kUInt_GrVertexAttribType, + + kUShort_norm_GrVertexAttribType, + + kUShort4_norm_GrVertexAttribType, // vector of 4 unsigned shorts. 0 -> 0.0f, 65535 -> 1.0f. + + kLast_GrVertexAttribType = kUShort4_norm_GrVertexAttribType +}; +static const int kGrVertexAttribTypeCount = kLast_GrVertexAttribType + 1; + +////////////////////////////////////////////////////////////////////////////// + +/** + * We have coverage effects that clip rendering to the edge of some geometric primitive. + * This enum specifies how that clipping is performed. Not all factories that take a + * GrClipEdgeType will succeed with all values and it is up to the caller to verify success. + */ +enum class GrClipEdgeType { + kFillBW, + kFillAA, + kInverseFillBW, + kInverseFillAA, + + kLast = kInverseFillAA +}; +static const int kGrClipEdgeTypeCnt = (int) GrClipEdgeType::kLast + 1; + +static constexpr bool GrClipEdgeTypeIsFill(const GrClipEdgeType edgeType) { + return (GrClipEdgeType::kFillAA == edgeType || GrClipEdgeType::kFillBW == edgeType); +} + +static constexpr bool GrClipEdgeTypeIsInverseFill(const GrClipEdgeType edgeType) { + return (GrClipEdgeType::kInverseFillAA == edgeType || + GrClipEdgeType::kInverseFillBW == edgeType); +} + +static constexpr bool GrClipEdgeTypeIsAA(const GrClipEdgeType edgeType) { + return (GrClipEdgeType::kFillBW != edgeType && + GrClipEdgeType::kInverseFillBW != edgeType); +} + +static inline GrClipEdgeType GrInvertClipEdgeType(const GrClipEdgeType edgeType) { + switch (edgeType) { + case GrClipEdgeType::kFillBW: + return GrClipEdgeType::kInverseFillBW; + case GrClipEdgeType::kFillAA: + return GrClipEdgeType::kInverseFillAA; + case GrClipEdgeType::kInverseFillBW: + return GrClipEdgeType::kFillBW; + case GrClipEdgeType::kInverseFillAA: + return GrClipEdgeType::kFillAA; + } + SkUNREACHABLE; +} + +/** + * Indicates the type of pending IO operations that can be recorded for gpu resources. + */ +enum GrIOType { + kRead_GrIOType, + kWrite_GrIOType, + kRW_GrIOType +}; + +/** + * Indicates the type of data that a GPU buffer will be used for. + */ +enum class GrGpuBufferType { + kVertex, + kIndex, + kDrawIndirect, + kXferCpuToGpu, + kXferGpuToCpu, + kUniform, +}; +static const constexpr int kGrGpuBufferTypeCount = static_cast(GrGpuBufferType::kUniform) + 1; + +/** + * Provides a performance hint regarding the frequency at which a data store will be accessed. + */ +enum GrAccessPattern { + /** Data store will be respecified repeatedly and used many times. */ + kDynamic_GrAccessPattern, + /** Data store will be specified once and used many times. (Thus disqualified from caching.) */ + kStatic_GrAccessPattern, + /** Data store will be specified once and used at most a few times. (Also can't be cached.) */ + kStream_GrAccessPattern, + + kLast_GrAccessPattern = kStream_GrAccessPattern +}; + +// Flags shared between the GrSurface & GrSurfaceProxy class hierarchies +enum class GrInternalSurfaceFlags { + kNone = 0, + + // Texture-level + + // Means the pixels in the texture are read-only. Cannot also be a GrRenderTarget[Proxy]. + kReadOnly = 1 << 0, + + // RT-level + + // This flag is for use with GL only. It tells us that the internal render target wraps FBO 0. + kGLRTFBOIDIs0 = 1 << 1, + + // This means the render target is multisampled, and internally holds a non-msaa texture for + // resolving into. The render target resolves itself by blitting into this internal texture. + // (asTexture() might or might not return the internal texture, but if it does, we always + // resolve the render target before accessing this texture's data.) + kRequiresManualMSAAResolve = 1 << 2, + + // This means the pixels in the render target are write-only. This is used for Dawn and Metal + // swap chain targets which can be rendered to, but not read or copied. + kFramebufferOnly = 1 << 3, + + // This is a Vulkan only flag. If set the surface can be used as an input attachment in a + // shader. This is used for doing in shader blending where we want to sample from the same + // image we are drawing to. + kVkRTSupportsInputAttachment = 1 << 4, +}; + +GR_MAKE_BITFIELD_CLASS_OPS(GrInternalSurfaceFlags) + +// 'GR_MAKE_BITFIELD_CLASS_OPS' defines the & operator on GrInternalSurfaceFlags to return bool. +// We want to find the bitwise & with these masks, so we declare them as ints. +constexpr static int kGrInternalTextureFlagsMask = static_cast( + GrInternalSurfaceFlags::kReadOnly); + +// We don't include kVkRTSupportsInputAttachment in this mask since we check it manually. We don't +// require that both the surface and proxy have matching values for this flag. Instead we require +// if the proxy has it set then the surface must also have it set. All other flags listed here must +// match on the proxy and surface. +// TODO: Add back kFramebufferOnly flag here once we update GrSurfaceCharacterization to take it +// as a flag. skbug.com/10672 +constexpr static int kGrInternalRenderTargetFlagsMask = static_cast( + GrInternalSurfaceFlags::kGLRTFBOIDIs0 | + GrInternalSurfaceFlags::kRequiresManualMSAAResolve/* | + GrInternalSurfaceFlags::kFramebufferOnly*/); + +constexpr static int kGrInternalTextureRenderTargetFlagsMask = + kGrInternalTextureFlagsMask | kGrInternalRenderTargetFlagsMask; + +#ifdef SK_DEBUG +// Takes a pointer to a GrCaps, and will suppress prints if required +#define GrCapsDebugf(caps, ...) if (!(caps)->suppressPrints()) SkDebugf(__VA_ARGS__) +#else +#define GrCapsDebugf(caps, ...) do {} while (0) +#endif + +/** + * Specifies if the holder owns the backend, OpenGL or Vulkan, object. + */ +enum class GrBackendObjectOwnership : bool { + /** Holder does not destroy the backend object. */ + kBorrowed = false, + /** Holder destroys the backend object. */ + kOwned = true +}; + +/** + * Used to include or exclude specific GPU path renderers for testing purposes. + */ +enum class GpuPathRenderers { + kNone = 0, // Always use software masks and/or DefaultPathRenderer. + kDashLine = 1 << 0, + kAtlas = 1 << 1, + kTessellation = 1 << 2, + kCoverageCounting = 1 << 3, + kAAHairline = 1 << 4, + kAAConvex = 1 << 5, + kAALinearizing = 1 << 6, + kSmall = 1 << 7, + kTriangulating = 1 << 8, + kDefault = ((1 << 9) - 1) // All path renderers. +}; + +/** + * Used to describe the current state of Mips on a GrTexture + */ +enum class GrMipmapStatus { + kNotAllocated, // Mips have not been allocated + kDirty, // Mips are allocated but the full mip tree does not have valid data + kValid, // All levels fully allocated and have valid data in them +}; + +GR_MAKE_BITFIELD_CLASS_OPS(GpuPathRenderers) + +/** + * Like SkColorType this describes a layout of pixel data in CPU memory. It specifies the channels, + * their type, and width. This exists so that the GPU backend can have private types that have no + * analog in the public facing SkColorType enum and omit types not implemented in the GPU backend. + * It does not refer to a texture format and the mapping to texture formats may be many-to-many. + * It does not specify the sRGB encoding of the stored values. The components are listed in order of + * where they appear in memory. In other words the first component listed is in the low bits and + * the last component in the high bits. + */ +enum class GrColorType { + kUnknown, + kAlpha_8, + kBGR_565, + kRGB_565, + kABGR_4444, // This name differs from SkColorType. kARGB_4444_SkColorType is misnamed. + kRGBA_8888, + kRGBA_8888_SRGB, + kRGB_888x, + kRG_88, + kBGRA_8888, + kRGBA_1010102, + kBGRA_1010102, + kRGBA_10x6, + kGray_8, + kGrayAlpha_88, + kAlpha_F16, + kRGBA_F16, + kRGBA_F16_Clamped, + kRGBA_F32, + + kAlpha_16, + kRG_1616, + kRG_F16, + kRGBA_16161616, + + // Unusual types that come up after reading back in cases where we are reassigning the meaning + // of a texture format's channels to use for a particular color format but have to read back the + // data to a full RGBA quadruple. (e.g. using a R8 texture format as A8 color type but the API + // only supports reading to RGBA8.) None of these have SkColorType equivalents. + kAlpha_8xxx, + kAlpha_F32xxx, + kGray_8xxx, + kR_8xxx, + + // Types used to initialize backend textures. + kRGB_888, + kR_8, + kR_16, + kR_F16, + kGray_F16, + kBGRA_4444, + kARGB_4444, + + kLast = kARGB_4444 +}; + +static const int kGrColorTypeCnt = static_cast(GrColorType::kLast) + 1; + +static constexpr SkColorType GrColorTypeToSkColorType(GrColorType ct) { + switch (ct) { + case GrColorType::kUnknown: return kUnknown_SkColorType; + case GrColorType::kAlpha_8: return kAlpha_8_SkColorType; + case GrColorType::kBGR_565: return kRGB_565_SkColorType; + case GrColorType::kRGB_565: return kUnknown_SkColorType; + case GrColorType::kABGR_4444: return kARGB_4444_SkColorType; + case GrColorType::kRGBA_8888: return kRGBA_8888_SkColorType; + case GrColorType::kRGBA_8888_SRGB: return kSRGBA_8888_SkColorType; + case GrColorType::kRGB_888x: return kRGB_888x_SkColorType; + case GrColorType::kRG_88: return kR8G8_unorm_SkColorType; + case GrColorType::kBGRA_8888: return kBGRA_8888_SkColorType; + case GrColorType::kRGBA_1010102: return kRGBA_1010102_SkColorType; + case GrColorType::kBGRA_1010102: return kBGRA_1010102_SkColorType; + case GrColorType::kRGBA_10x6: return kRGBA_10x6_SkColorType; + case GrColorType::kGray_8: return kGray_8_SkColorType; + case GrColorType::kGrayAlpha_88: return kUnknown_SkColorType; + case GrColorType::kAlpha_F16: return kA16_float_SkColorType; + case GrColorType::kRGBA_F16: return kRGBA_F16_SkColorType; + case GrColorType::kRGBA_F16_Clamped: return kRGBA_F16Norm_SkColorType; + case GrColorType::kRGBA_F32: return kRGBA_F32_SkColorType; + case GrColorType::kAlpha_8xxx: return kUnknown_SkColorType; + case GrColorType::kAlpha_F32xxx: return kUnknown_SkColorType; + case GrColorType::kGray_8xxx: return kUnknown_SkColorType; + case GrColorType::kR_8xxx: return kUnknown_SkColorType; + case GrColorType::kAlpha_16: return kA16_unorm_SkColorType; + case GrColorType::kRG_1616: return kR16G16_unorm_SkColorType; + case GrColorType::kRGBA_16161616: return kR16G16B16A16_unorm_SkColorType; + case GrColorType::kRG_F16: return kR16G16_float_SkColorType; + case GrColorType::kRGB_888: return kUnknown_SkColorType; + case GrColorType::kR_8: return kR8_unorm_SkColorType; + case GrColorType::kR_16: return kUnknown_SkColorType; + case GrColorType::kR_F16: return kUnknown_SkColorType; + case GrColorType::kGray_F16: return kUnknown_SkColorType; + case GrColorType::kARGB_4444: return kUnknown_SkColorType; + case GrColorType::kBGRA_4444: return kUnknown_SkColorType; + } + SkUNREACHABLE; +} + +static constexpr GrColorType SkColorTypeToGrColorType(SkColorType ct) { + switch (ct) { + case kUnknown_SkColorType: return GrColorType::kUnknown; + case kAlpha_8_SkColorType: return GrColorType::kAlpha_8; + case kRGB_565_SkColorType: return GrColorType::kBGR_565; + case kARGB_4444_SkColorType: return GrColorType::kABGR_4444; + case kRGBA_8888_SkColorType: return GrColorType::kRGBA_8888; + case kSRGBA_8888_SkColorType: return GrColorType::kRGBA_8888_SRGB; + case kRGB_888x_SkColorType: return GrColorType::kRGB_888x; + case kBGRA_8888_SkColorType: return GrColorType::kBGRA_8888; + case kGray_8_SkColorType: return GrColorType::kGray_8; + case kRGBA_F16Norm_SkColorType: return GrColorType::kRGBA_F16_Clamped; + case kRGBA_F16_SkColorType: return GrColorType::kRGBA_F16; + case kRGBA_1010102_SkColorType: return GrColorType::kRGBA_1010102; + case kRGB_101010x_SkColorType: return GrColorType::kUnknown; + case kBGRA_1010102_SkColorType: return GrColorType::kBGRA_1010102; + case kBGR_101010x_SkColorType: return GrColorType::kUnknown; + case kBGR_101010x_XR_SkColorType: return GrColorType::kUnknown; + case kBGRA_10101010_XR_SkColorType: return GrColorType::kUnknown; + case kRGBA_10x6_SkColorType: return GrColorType::kRGBA_10x6; + case kRGBA_F32_SkColorType: return GrColorType::kRGBA_F32; + case kR8G8_unorm_SkColorType: return GrColorType::kRG_88; + case kA16_unorm_SkColorType: return GrColorType::kAlpha_16; + case kR16G16_unorm_SkColorType: return GrColorType::kRG_1616; + case kA16_float_SkColorType: return GrColorType::kAlpha_F16; + case kR16G16_float_SkColorType: return GrColorType::kRG_F16; + case kR16G16B16A16_unorm_SkColorType: return GrColorType::kRGBA_16161616; + case kR8_unorm_SkColorType: return GrColorType::kR_8; + } + SkUNREACHABLE; +} + +static constexpr uint32_t GrColorTypeChannelFlags(GrColorType ct) { + switch (ct) { + case GrColorType::kUnknown: return 0; + case GrColorType::kAlpha_8: return kAlpha_SkColorChannelFlag; + case GrColorType::kBGR_565: return kRGB_SkColorChannelFlags; + case GrColorType::kRGB_565: return kRGB_SkColorChannelFlags; + case GrColorType::kABGR_4444: return kRGBA_SkColorChannelFlags; + case GrColorType::kRGBA_8888: return kRGBA_SkColorChannelFlags; + case GrColorType::kRGBA_8888_SRGB: return kRGBA_SkColorChannelFlags; + case GrColorType::kRGB_888x: return kRGB_SkColorChannelFlags; + case GrColorType::kRG_88: return kRG_SkColorChannelFlags; + case GrColorType::kBGRA_8888: return kRGBA_SkColorChannelFlags; + case GrColorType::kRGBA_1010102: return kRGBA_SkColorChannelFlags; + case GrColorType::kBGRA_1010102: return kRGBA_SkColorChannelFlags; + case GrColorType::kRGBA_10x6: return kRGBA_SkColorChannelFlags; + case GrColorType::kGray_8: return kGray_SkColorChannelFlag; + case GrColorType::kGrayAlpha_88: return kGrayAlpha_SkColorChannelFlags; + case GrColorType::kAlpha_F16: return kAlpha_SkColorChannelFlag; + case GrColorType::kRGBA_F16: return kRGBA_SkColorChannelFlags; + case GrColorType::kRGBA_F16_Clamped: return kRGBA_SkColorChannelFlags; + case GrColorType::kRGBA_F32: return kRGBA_SkColorChannelFlags; + case GrColorType::kAlpha_8xxx: return kAlpha_SkColorChannelFlag; + case GrColorType::kAlpha_F32xxx: return kAlpha_SkColorChannelFlag; + case GrColorType::kGray_8xxx: return kGray_SkColorChannelFlag; + case GrColorType::kR_8xxx: return kRed_SkColorChannelFlag; + case GrColorType::kAlpha_16: return kAlpha_SkColorChannelFlag; + case GrColorType::kRG_1616: return kRG_SkColorChannelFlags; + case GrColorType::kRGBA_16161616: return kRGBA_SkColorChannelFlags; + case GrColorType::kRG_F16: return kRG_SkColorChannelFlags; + case GrColorType::kRGB_888: return kRGB_SkColorChannelFlags; + case GrColorType::kR_8: return kRed_SkColorChannelFlag; + case GrColorType::kR_16: return kRed_SkColorChannelFlag; + case GrColorType::kR_F16: return kRed_SkColorChannelFlag; + case GrColorType::kGray_F16: return kGray_SkColorChannelFlag; + case GrColorType::kARGB_4444: return kRGBA_SkColorChannelFlags; + case GrColorType::kBGRA_4444: return kRGBA_SkColorChannelFlags; + } + SkUNREACHABLE; +} + +/** + * Describes the encoding of channel data in a GrColorType. + */ +enum class GrColorTypeEncoding { + kUnorm, + kSRGBUnorm, + // kSnorm, + kFloat, + // kSint + // kUint +}; + +/** + * Describes a GrColorType by how many bits are used for each color component and how they are + * encoded. Currently all the non-zero channels share a single GrColorTypeEncoding. This could be + * expanded to store separate encodings and to indicate which bits belong to which components. + */ +class GrColorFormatDesc { +public: + static constexpr GrColorFormatDesc MakeRGBA(int rgba, GrColorTypeEncoding e) { + return {rgba, rgba, rgba, rgba, 0, e}; + } + + static constexpr GrColorFormatDesc MakeRGBA(int rgb, int a, GrColorTypeEncoding e) { + return {rgb, rgb, rgb, a, 0, e}; + } + + static constexpr GrColorFormatDesc MakeRGB(int rgb, GrColorTypeEncoding e) { + return {rgb, rgb, rgb, 0, 0, e}; + } + + static constexpr GrColorFormatDesc MakeRGB(int r, int g, int b, GrColorTypeEncoding e) { + return {r, g, b, 0, 0, e}; + } + + static constexpr GrColorFormatDesc MakeAlpha(int a, GrColorTypeEncoding e) { + return {0, 0, 0, a, 0, e}; + } + + static constexpr GrColorFormatDesc MakeR(int r, GrColorTypeEncoding e) { + return {r, 0, 0, 0, 0, e}; + } + + static constexpr GrColorFormatDesc MakeRG(int rg, GrColorTypeEncoding e) { + return {rg, rg, 0, 0, 0, e}; + } + + static constexpr GrColorFormatDesc MakeGray(int grayBits, GrColorTypeEncoding e) { + return {0, 0, 0, 0, grayBits, e}; + } + + static constexpr GrColorFormatDesc MakeGrayAlpha(int grayAlpha, GrColorTypeEncoding e) { + return {0, 0, 0, 0, grayAlpha, e}; + } + + static constexpr GrColorFormatDesc MakeInvalid() { return {}; } + + constexpr int r() const { return fRBits; } + constexpr int g() const { return fGBits; } + constexpr int b() const { return fBBits; } + constexpr int a() const { return fABits; } + constexpr int operator[](int c) const { + switch (c) { + case 0: return this->r(); + case 1: return this->g(); + case 2: return this->b(); + case 3: return this->a(); + } + SkUNREACHABLE; + } + + constexpr int gray() const { return fGrayBits; } + + constexpr GrColorTypeEncoding encoding() const { return fEncoding; } + +private: + int fRBits = 0; + int fGBits = 0; + int fBBits = 0; + int fABits = 0; + int fGrayBits = 0; + GrColorTypeEncoding fEncoding = GrColorTypeEncoding::kUnorm; + + constexpr GrColorFormatDesc() = default; + + constexpr GrColorFormatDesc(int r, int g, int b, int a, int gray, GrColorTypeEncoding encoding) + : fRBits(r), fGBits(g), fBBits(b), fABits(a), fGrayBits(gray), fEncoding(encoding) { + SkASSERT(r >= 0 && g >= 0 && b >= 0 && a >= 0 && gray >= 0); + SkASSERT(!gray || (!r && !g && !b)); + SkASSERT(r || g || b || a || gray); + } +}; + +static constexpr GrColorFormatDesc GrGetColorTypeDesc(GrColorType ct) { + switch (ct) { + case GrColorType::kUnknown: + return GrColorFormatDesc::MakeInvalid(); + case GrColorType::kAlpha_8: + return GrColorFormatDesc::MakeAlpha(8, GrColorTypeEncoding::kUnorm); + case GrColorType::kBGR_565: + return GrColorFormatDesc::MakeRGB(5, 6, 5, GrColorTypeEncoding::kUnorm); + case GrColorType::kRGB_565: + return GrColorFormatDesc::MakeRGB(5, 6, 5, GrColorTypeEncoding::kUnorm); + case GrColorType::kABGR_4444: + return GrColorFormatDesc::MakeRGBA(4, GrColorTypeEncoding::kUnorm); + case GrColorType::kRGBA_8888: + return GrColorFormatDesc::MakeRGBA(8, GrColorTypeEncoding::kUnorm); + case GrColorType::kRGBA_8888_SRGB: + return GrColorFormatDesc::MakeRGBA(8, GrColorTypeEncoding::kSRGBUnorm); + case GrColorType::kRGB_888x: + return GrColorFormatDesc::MakeRGB(8, GrColorTypeEncoding::kUnorm); + case GrColorType::kRG_88: + return GrColorFormatDesc::MakeRG(8, GrColorTypeEncoding::kUnorm); + case GrColorType::kBGRA_8888: + return GrColorFormatDesc::MakeRGBA(8, GrColorTypeEncoding::kUnorm); + case GrColorType::kRGBA_1010102: + return GrColorFormatDesc::MakeRGBA(10, 2, GrColorTypeEncoding::kUnorm); + case GrColorType::kBGRA_1010102: + return GrColorFormatDesc::MakeRGBA(10, 2, GrColorTypeEncoding::kUnorm); + case GrColorType::kRGBA_10x6: + return GrColorFormatDesc::MakeRGBA(10, GrColorTypeEncoding::kUnorm); + case GrColorType::kGray_8: + return GrColorFormatDesc::MakeGray(8, GrColorTypeEncoding::kUnorm); + case GrColorType::kGrayAlpha_88: + return GrColorFormatDesc::MakeGrayAlpha(8, GrColorTypeEncoding::kUnorm); + case GrColorType::kAlpha_F16: + return GrColorFormatDesc::MakeAlpha(16, GrColorTypeEncoding::kFloat); + case GrColorType::kRGBA_F16: + return GrColorFormatDesc::MakeRGBA(16, GrColorTypeEncoding::kFloat); + case GrColorType::kRGBA_F16_Clamped: + return GrColorFormatDesc::MakeRGBA(16, GrColorTypeEncoding::kFloat); + case GrColorType::kRGBA_F32: + return GrColorFormatDesc::MakeRGBA(32, GrColorTypeEncoding::kFloat); + case GrColorType::kAlpha_8xxx: + return GrColorFormatDesc::MakeAlpha(8, GrColorTypeEncoding::kUnorm); + case GrColorType::kAlpha_F32xxx: + return GrColorFormatDesc::MakeAlpha(32, GrColorTypeEncoding::kFloat); + case GrColorType::kGray_8xxx: + return GrColorFormatDesc::MakeGray(8, GrColorTypeEncoding::kUnorm); + case GrColorType::kR_8xxx: + return GrColorFormatDesc::MakeR(8, GrColorTypeEncoding::kUnorm); + case GrColorType::kAlpha_16: + return GrColorFormatDesc::MakeAlpha(16, GrColorTypeEncoding::kUnorm); + case GrColorType::kRG_1616: + return GrColorFormatDesc::MakeRG(16, GrColorTypeEncoding::kUnorm); + case GrColorType::kRGBA_16161616: + return GrColorFormatDesc::MakeRGBA(16, GrColorTypeEncoding::kUnorm); + case GrColorType::kRG_F16: + return GrColorFormatDesc::MakeRG(16, GrColorTypeEncoding::kFloat); + case GrColorType::kRGB_888: + return GrColorFormatDesc::MakeRGB(8, GrColorTypeEncoding::kUnorm); + case GrColorType::kR_8: + return GrColorFormatDesc::MakeR(8, GrColorTypeEncoding::kUnorm); + case GrColorType::kR_16: + return GrColorFormatDesc::MakeR(16, GrColorTypeEncoding::kUnorm); + case GrColorType::kR_F16: + return GrColorFormatDesc::MakeR(16, GrColorTypeEncoding::kFloat); + case GrColorType::kGray_F16: + return GrColorFormatDesc::MakeGray(16, GrColorTypeEncoding::kFloat); + case GrColorType::kARGB_4444: + return GrColorFormatDesc::MakeRGBA(4, GrColorTypeEncoding::kUnorm); + case GrColorType::kBGRA_4444: + return GrColorFormatDesc::MakeRGBA(4, GrColorTypeEncoding::kUnorm); + } + SkUNREACHABLE; +} + +static constexpr GrClampType GrColorTypeClampType(GrColorType colorType) { + if (GrGetColorTypeDesc(colorType).encoding() == GrColorTypeEncoding::kUnorm || + GrGetColorTypeDesc(colorType).encoding() == GrColorTypeEncoding::kSRGBUnorm) { + return GrClampType::kAuto; + } + return GrColorType::kRGBA_F16_Clamped == colorType ? GrClampType::kManual : GrClampType::kNone; +} + +// Consider a color type "wider" than n if it has more than n bits for any its representable +// channels. +static constexpr bool GrColorTypeIsWiderThan(GrColorType colorType, int n) { + SkASSERT(n > 0); + auto desc = GrGetColorTypeDesc(colorType); + return (desc.r() && desc.r() > n )|| + (desc.g() && desc.g() > n) || + (desc.b() && desc.b() > n) || + (desc.a() && desc.a() > n) || + (desc.gray() && desc.gray() > n); +} + +static constexpr bool GrColorTypeIsAlphaOnly(GrColorType ct) { + return GrColorTypeChannelFlags(ct) == kAlpha_SkColorChannelFlag; +} + +static constexpr bool GrColorTypeHasAlpha(GrColorType ct) { + return GrColorTypeChannelFlags(ct) & kAlpha_SkColorChannelFlag; +} + +static constexpr size_t GrColorTypeBytesPerPixel(GrColorType ct) { + switch (ct) { + case GrColorType::kUnknown: return 0; + case GrColorType::kAlpha_8: return 1; + case GrColorType::kBGR_565: return 2; + case GrColorType::kRGB_565: return 2; + case GrColorType::kABGR_4444: return 2; + case GrColorType::kRGBA_8888: return 4; + case GrColorType::kRGBA_8888_SRGB: return 4; + case GrColorType::kRGB_888x: return 4; + case GrColorType::kRG_88: return 2; + case GrColorType::kBGRA_8888: return 4; + case GrColorType::kRGBA_1010102: return 4; + case GrColorType::kBGRA_1010102: return 4; + case GrColorType::kRGBA_10x6: return 8; + case GrColorType::kGray_8: return 1; + case GrColorType::kGrayAlpha_88: return 2; + case GrColorType::kAlpha_F16: return 2; + case GrColorType::kRGBA_F16: return 8; + case GrColorType::kRGBA_F16_Clamped: return 8; + case GrColorType::kRGBA_F32: return 16; + case GrColorType::kAlpha_8xxx: return 4; + case GrColorType::kAlpha_F32xxx: return 16; + case GrColorType::kGray_8xxx: return 4; + case GrColorType::kR_8xxx: return 4; + case GrColorType::kAlpha_16: return 2; + case GrColorType::kRG_1616: return 4; + case GrColorType::kRGBA_16161616: return 8; + case GrColorType::kRG_F16: return 4; + case GrColorType::kRGB_888: return 3; + case GrColorType::kR_8: return 1; + case GrColorType::kR_16: return 2; + case GrColorType::kR_F16: return 2; + case GrColorType::kGray_F16: return 2; + case GrColorType::kARGB_4444: return 2; + case GrColorType::kBGRA_4444: return 2; + } + SkUNREACHABLE; +} + +enum class GrDstSampleFlags { + kNone = 0, + kRequiresTextureBarrier = 1 << 0, + kAsInputAttachment = 1 << 1, +}; +GR_MAKE_BITFIELD_CLASS_OPS(GrDstSampleFlags) + +using GrVisitProxyFunc = std::function; + +#if defined(SK_DEBUG) || defined(GR_TEST_UTILS) || defined(SK_ENABLE_DUMP_GPU) +static constexpr const char* GrBackendApiToStr(GrBackendApi api) { + switch (api) { + case GrBackendApi::kOpenGL: return "OpenGL"; + case GrBackendApi::kVulkan: return "Vulkan"; + case GrBackendApi::kMetal: return "Metal"; + case GrBackendApi::kDirect3D: return "Direct3D"; + case GrBackendApi::kMock: return "Mock"; + case GrBackendApi::kUnsupported: return "Unsupported"; + } + SkUNREACHABLE; +} + +static constexpr const char* GrColorTypeToStr(GrColorType ct) { + switch (ct) { + case GrColorType::kUnknown: return "kUnknown"; + case GrColorType::kAlpha_8: return "kAlpha_8"; + case GrColorType::kBGR_565: return "kBGR_565"; + case GrColorType::kRGB_565: return "kRGB_565"; + case GrColorType::kABGR_4444: return "kABGR_4444"; + case GrColorType::kRGBA_8888: return "kRGBA_8888"; + case GrColorType::kRGBA_8888_SRGB: return "kRGBA_8888_SRGB"; + case GrColorType::kRGB_888x: return "kRGB_888x"; + case GrColorType::kRG_88: return "kRG_88"; + case GrColorType::kBGRA_8888: return "kBGRA_8888"; + case GrColorType::kRGBA_1010102: return "kRGBA_1010102"; + case GrColorType::kBGRA_1010102: return "kBGRA_1010102"; + case GrColorType::kRGBA_10x6: return "kBGRA_10x6"; + case GrColorType::kGray_8: return "kGray_8"; + case GrColorType::kGrayAlpha_88: return "kGrayAlpha_88"; + case GrColorType::kAlpha_F16: return "kAlpha_F16"; + case GrColorType::kRGBA_F16: return "kRGBA_F16"; + case GrColorType::kRGBA_F16_Clamped: return "kRGBA_F16_Clamped"; + case GrColorType::kRGBA_F32: return "kRGBA_F32"; + case GrColorType::kAlpha_8xxx: return "kAlpha_8xxx"; + case GrColorType::kAlpha_F32xxx: return "kAlpha_F32xxx"; + case GrColorType::kGray_8xxx: return "kGray_8xxx"; + case GrColorType::kR_8xxx: return "kR_8xxx"; + case GrColorType::kAlpha_16: return "kAlpha_16"; + case GrColorType::kRG_1616: return "kRG_1616"; + case GrColorType::kRGBA_16161616: return "kRGBA_16161616"; + case GrColorType::kRG_F16: return "kRG_F16"; + case GrColorType::kRGB_888: return "kRGB_888"; + case GrColorType::kR_8: return "kR_8"; + case GrColorType::kR_16: return "kR_16"; + case GrColorType::kR_F16: return "kR_F16"; + case GrColorType::kGray_F16: return "kGray_F16"; + case GrColorType::kARGB_4444: return "kARGB_4444"; + case GrColorType::kBGRA_4444: return "kBGRA_4444"; + } + SkUNREACHABLE; +} + +static constexpr const char* GrSurfaceOriginToStr(GrSurfaceOrigin origin) { + switch (origin) { + case kTopLeft_GrSurfaceOrigin: return "kTopLeft"; + case kBottomLeft_GrSurfaceOrigin: return "kBottomLeft"; + } + SkUNREACHABLE; +} +#endif + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/graphite/ContextOptionsPriv.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/graphite/ContextOptionsPriv.h new file mode 100644 index 00000000000..769af794976 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/graphite/ContextOptionsPriv.h @@ -0,0 +1,70 @@ +/* + * Copyright 2023 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef skgpu_graphite_ContextOptionsPriv_DEFINED +#define skgpu_graphite_ContextOptionsPriv_DEFINED + +namespace skgpu::graphite { + +/** + * Used to include or exclude a specific path rendering technique for testing purposes. + */ +enum class PathRendererStrategy { + /** + * Graphite selects the best path rendering technique for each shape. This is the default + * behavior. + */ + kDefault, + + /** + * All paths are rasterized into coverage masks using a GPU compute approach. This method + * always uses analytic anti-aliasing. + */ + kComputeAnalyticAA, + + /** + * All paths are rasterized into coverage masks using a GPU compute approach. This method + * supports 16 and 8 sample multi-sampled anti-aliasing. + */ + kComputeMSAA16, + kComputeMSAA8, + + /** + * All paths are rasterized into coverage masks using the CPU raster backend. + */ + kRasterAA, + + /** + * Render paths using tessellation and stencil-and-cover. + */ + kTessellation, +}; + +/** + * Private options that are only meant for testing within Skia's tools. + */ +struct ContextOptionsPriv { + + int fMaxTextureSizeOverride = SK_MaxS32; + + /** + * Maximum width and height of internal texture atlases. + */ + int fMaxTextureAtlasSize = 2048; + + /** + * If true, will store a pointer in Recorder that points back to the Context + * that created it. Used by readPixels() and other methods that normally require a Context. + */ + bool fStoreContextRefInRecorder = false; + + PathRendererStrategy fPathRendererStrategy = PathRendererStrategy::kDefault; +}; + +} // namespace skgpu::graphite + +#endif // skgpu_graphite_ContextOptionsPriv_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/graphite/DawnTypesPriv.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/graphite/DawnTypesPriv.h new file mode 100644 index 00000000000..c3c4aa0bbb4 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/graphite/DawnTypesPriv.h @@ -0,0 +1,61 @@ +/* + * Copyright 2022 Google LLC. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef skgpu_graphite_DawnTypesPriv_DEFINED +#define skgpu_graphite_DawnTypesPriv_DEFINED + +#include "include/core/SkString.h" +#include "include/gpu/graphite/dawn/DawnTypes.h" + +namespace skgpu::graphite { + +struct DawnTextureSpec { + DawnTextureSpec() = default; + DawnTextureSpec(const DawnTextureInfo& info) + : fFormat(info.fFormat) + , fViewFormat(info.fViewFormat) + , fUsage(info.fUsage) + , fAspect(info.fAspect) + , fSlice(info.fSlice) {} + + bool operator==(const DawnTextureSpec& that) const { + return fUsage == that.fUsage && fFormat == that.fFormat && + fViewFormat == that.fViewFormat && fAspect == that.fAspect && + fSlice == that.fSlice; + } + + bool isCompatible(const DawnTextureSpec& that) const { + // The usages may match or the usage passed in may be a superset of the usage stored within. + // The aspect should either match the plane aspect or should be All. + return getViewFormat() == that.getViewFormat() && (fUsage & that.fUsage) == fUsage && + (fAspect == that.fAspect || fAspect == wgpu::TextureAspect::All); + } + + wgpu::TextureFormat getViewFormat() const { + return fViewFormat != wgpu::TextureFormat::Undefined ? fViewFormat : fFormat; + } + + SkString toString() const; + + wgpu::TextureFormat fFormat = wgpu::TextureFormat::Undefined; + // `fViewFormat` is always single plane format or plane view format for a multiplanar + // wgpu::Texture. + wgpu::TextureFormat fViewFormat = wgpu::TextureFormat::Undefined; + wgpu::TextureUsage fUsage = wgpu::TextureUsage::None; + wgpu::TextureAspect fAspect = wgpu::TextureAspect::All; + uint32_t fSlice = 0; +}; + +DawnTextureInfo DawnTextureSpecToTextureInfo(const DawnTextureSpec& dawnSpec, + uint32_t sampleCount, + Mipmapped mipmapped); + +DawnTextureInfo DawnTextureInfoFromWGPUTexture(WGPUTexture texture); + +} // namespace skgpu::graphite + +#endif // skgpu_graphite_DawnTypesPriv_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/graphite/MtlGraphiteTypesPriv.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/graphite/MtlGraphiteTypesPriv.h new file mode 100644 index 00000000000..56059b6a525 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/graphite/MtlGraphiteTypesPriv.h @@ -0,0 +1,95 @@ +/* + * Copyright 2021 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef skgpu_graphite_MtlGraphiteTypesPriv_DEFINED +#define skgpu_graphite_MtlGraphiteTypesPriv_DEFINED + +#include "include/core/SkString.h" +#include "include/gpu/graphite/GraphiteTypes.h" +#include "include/gpu/graphite/mtl/MtlGraphiteTypes.h" + +/////////////////////////////////////////////////////////////////////////////// + +#ifdef __APPLE__ + +#include + +// We're using the MSL version as shorthand for the Metal SDK version here +#if defined(SK_BUILD_FOR_MAC) +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 130000 +#define SKGPU_GRAPHITE_METAL_SDK_VERSION 300 +#elif __MAC_OS_X_VERSION_MAX_ALLOWED >= 120000 +#define SKGPU_GRAPHITE_METAL_SDK_VERSION 240 +#elif __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 +#define SKGPU_GRAPHITE_METAL_SDK_VERSION 230 +#else +#error Must use at least 11.00 SDK to build Metal backend for MacOS +#endif +#else +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 160000 || __TV_OS_VERSION_MAX_ALLOWED >= 160000 +#define SKGPU_GRAPHITE_METAL_SDK_VERSION 300 +#elif __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000 || __TV_OS_VERSION_MAX_ALLOWED >= 150000 +#define SKGPU_GRAPHITE_METAL_SDK_VERSION 240 +#elif __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 || __TV_OS_VERSION_MAX_ALLOWED >= 140000 +#define SKGPU_GRAPHITE_METAL_SDK_VERSION 230 +#else +#error Must use at least 14.00 SDK to build Metal backend for iOS +#endif +#endif + +#endif // __APPLE__ + +namespace skgpu::graphite { + +struct MtlTextureSpec { + MtlTextureSpec() + : fFormat(0) + , fUsage(0) + , fStorageMode(0) + , fFramebufferOnly(false) {} + MtlTextureSpec(const MtlTextureInfo& info) + : fFormat(info.fFormat) + , fUsage(info.fUsage) + , fStorageMode(info.fStorageMode) + , fFramebufferOnly(info.fFramebufferOnly) {} + + bool operator==(const MtlTextureSpec& that) const { + return fFormat == that.fFormat && + fUsage == that.fUsage && + fStorageMode == that.fStorageMode && + fFramebufferOnly == that.fFramebufferOnly; + } + + bool isCompatible(const MtlTextureSpec& that) const { + // The usages may match or the usage passed in may be a superset of the usage stored within. + return fFormat == that.fFormat && + fStorageMode == that.fStorageMode && + fFramebufferOnly == that.fFramebufferOnly && + (fUsage & that.fUsage) == fUsage; + } + + SkString toString() const { + return SkStringPrintf("format=%u,usage=0x%04X,storageMode=%u,framebufferOnly=%d", + fFormat, + fUsage, + fStorageMode, + fFramebufferOnly); + } + + MtlPixelFormat fFormat; + MtlTextureUsage fUsage; + MtlStorageMode fStorageMode; + bool fFramebufferOnly; +}; + +MtlTextureInfo MtlTextureSpecToTextureInfo(const MtlTextureSpec& mtlSpec, + uint32_t sampleCount, + Mipmapped mipmapped); + +} // namespace skgpu::graphite + +#endif // skgpu_graphite_MtlGraphiteTypesPriv_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/graphite/VulkanGraphiteTypesPriv.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/graphite/VulkanGraphiteTypesPriv.h new file mode 100644 index 00000000000..57019163b84 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/graphite/VulkanGraphiteTypesPriv.h @@ -0,0 +1,83 @@ +/* + * Copyright 2022 Google LLC. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef skgpu_graphite_VulkanGraphiteTypesPriv_DEFINED +#define skgpu_graphite_VulkanGraphiteTypesPriv_DEFINED + +#include "include/core/SkString.h" +#include "include/gpu/graphite/vk/VulkanGraphiteTypes.h" +#include "include/private/gpu/vk/SkiaVulkan.h" + +namespace skgpu::graphite { + +struct VulkanTextureSpec { + VulkanTextureSpec() + : fFlags(0) + , fFormat(VK_FORMAT_UNDEFINED) + , fImageTiling(VK_IMAGE_TILING_OPTIMAL) + , fImageUsageFlags(0) + , fSharingMode(VK_SHARING_MODE_EXCLUSIVE) + , fAspectMask(VK_IMAGE_ASPECT_COLOR_BIT) + , fYcbcrConversionInfo({}) {} + VulkanTextureSpec(const VulkanTextureInfo& info) + : fFlags(info.fFlags) + , fFormat(info.fFormat) + , fImageTiling(info.fImageTiling) + , fImageUsageFlags(info.fImageUsageFlags) + , fSharingMode(info.fSharingMode) + , fAspectMask(info.fAspectMask) + , fYcbcrConversionInfo(info.fYcbcrConversionInfo) {} + + bool operator==(const VulkanTextureSpec& that) const { + return fFlags == that.fFlags && + fFormat == that.fFormat && + fImageTiling == that.fImageTiling && + fImageUsageFlags == that.fImageUsageFlags && + fSharingMode == that.fSharingMode && + fAspectMask == that.fAspectMask && + fYcbcrConversionInfo == that.fYcbcrConversionInfo; + } + + bool isCompatible(const VulkanTextureSpec& that) const { + // The usages may match or the usage passed in may be a superset of the usage stored within. + return fFlags == that.fFlags && + fFormat == that.fFormat && + fImageTiling == that.fImageTiling && + fSharingMode == that.fSharingMode && + fAspectMask == that.fAspectMask && + (fImageUsageFlags & that.fImageUsageFlags) == fImageUsageFlags && + fYcbcrConversionInfo == that.fYcbcrConversionInfo; + } + + SkString toString() const { + return SkStringPrintf( + "flags=0x%08X,format=%d,imageTiling=%d,imageUsageFlags=0x%08X,sharingMode=%d," + "aspectMask=%u", + fFlags, + fFormat, + fImageTiling, + fImageUsageFlags, + fSharingMode, + fAspectMask); + } + + VkImageCreateFlags fFlags; + VkFormat fFormat; + VkImageTiling fImageTiling; + VkImageUsageFlags fImageUsageFlags; + VkSharingMode fSharingMode; + VkImageAspectFlags fAspectMask; + VulkanYcbcrConversionInfo fYcbcrConversionInfo; +}; + +VulkanTextureInfo VulkanTextureSpecToTextureInfo(const VulkanTextureSpec& vkSpec, + uint32_t sampleCount, + Mipmapped mipmapped); + +} // namespace skgpu::graphite + +#endif // skgpu_graphite_VulkanGraphiteTypesPriv_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/vk/SkiaVulkan.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/vk/SkiaVulkan.h new file mode 100644 index 00000000000..412dbf535fe --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/private/gpu/vk/SkiaVulkan.h @@ -0,0 +1,36 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkiaVulkan_DEFINED +#define SkiaVulkan_DEFINED + +#include "include/core/SkTypes.h" + +// IWYU pragma: begin_exports + +#if (SKIA_IMPLEMENTATION || !defined(SK_VULKAN)) && !defined(SK_USE_EXTERNAL_VULKAN_HEADERS) +#include "include/third_party/vulkan/vulkan/vulkan_core.h" +#else +// For google3 builds we don't set SKIA_IMPLEMENTATION so we need to make sure that the vulkan +// headers stay up to date for our needs +#include +#endif + +#ifdef SK_BUILD_FOR_ANDROID +// This is needed to get android extensions for external memory +#if (SKIA_IMPLEMENTATION || !defined(SK_VULKAN)) && !defined(SK_USE_EXTERNAL_VULKAN_HEADERS) +#include "include/third_party/vulkan/vulkan/vulkan_android.h" +#else +// For google3 builds we don't set SKIA_IMPLEMENTATION so we need to make sure that the vulkan +// headers stay up to date for our needs +#include +#endif +#endif + +// IWYU pragma: end_exports + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/sksl/OWNERS b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/sksl/OWNERS new file mode 100644 index 00000000000..9e9d9bb906d --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/sksl/OWNERS @@ -0,0 +1,3 @@ +# In addition to include/ owners, the following reviewers can approve changes to SkSL public API: +brianosman@google.com +johnstiles@google.com diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/sksl/SkSLDebugTrace.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/sksl/SkSLDebugTrace.h new file mode 100644 index 00000000000..9c5eafbc94e --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/sksl/SkSLDebugTrace.h @@ -0,0 +1,28 @@ +/* + * Copyright 2021 Google LLC. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SKSL_DEBUG_TRACE +#define SKSL_DEBUG_TRACE + +#include "include/core/SkRefCnt.h" + +class SkWStream; + +namespace SkSL { + +class DebugTrace : public SkRefCnt { +public: + /** Serializes a debug trace to JSON which can be parsed by our debugger. */ + virtual void writeTrace(SkWStream* w) const = 0; + + /** Generates a human-readable dump of the debug trace. */ + virtual void dump(SkWStream* o) const = 0; +}; + +} // namespace SkSL + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/sksl/SkSLVersion.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/sksl/SkSLVersion.h new file mode 100644 index 00000000000..ad059d580ef --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/sksl/SkSLVersion.h @@ -0,0 +1,27 @@ +/* + * Copyright 2022 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkSLVersion_DEFINED +#define SkSLVersion_DEFINED + +namespace SkSL { + +enum class Version { + /** + * Desktop GLSL 1.10, GLSL ES 1.00, WebGL 1.0 + */ + k100, + + /** + * Desktop GLSL 3.30, GLSL ES 3.00, WebGL 2.0 + */ + k300, +}; + +} // namespace SkSL + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/svg/SkSVGCanvas.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/svg/SkSVGCanvas.h new file mode 100644 index 00000000000..d4c38ea0177 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/svg/SkSVGCanvas.h @@ -0,0 +1,42 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkSVGCanvas_DEFINED +#define SkSVGCanvas_DEFINED + +#include "include/core/SkTypes.h" + +#include +#include + +class SkCanvas; +class SkWStream; +struct SkRect; + +class SK_API SkSVGCanvas { +public: + enum { + kConvertTextToPaths_Flag = 0x01, // emit text as s + kNoPrettyXML_Flag = 0x02, // suppress newlines and tabs in output + kRelativePathEncoding_Flag = 0x04, // use relative commands for path encoding + }; + + /** + * Returns a new canvas that will generate SVG commands from its draw calls, and send + * them to the provided stream. Ownership of the stream is not transfered, and it must + * remain valid for the lifetime of the returned canvas. + * + * The canvas may buffer some drawing calls, so the output is not guaranteed to be valid + * or complete until the canvas instance is deleted. + * + * The 'bounds' parameter defines an initial SVG viewport (viewBox attribute on the root + * SVG element). + */ + static std::unique_ptr Make(const SkRect& bounds, SkWStream*, uint32_t flags = 0); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkCamera.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkCamera.h new file mode 100644 index 00000000000..536691875e4 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkCamera.h @@ -0,0 +1,109 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +// Inspired by Rob Johnson's most excellent QuickDraw GX sample code + +#ifndef SkCamera_DEFINED +#define SkCamera_DEFINED + +#include "include/core/SkM44.h" +#include "include/core/SkMatrix.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkNoncopyable.h" + +// NOTE -- This entire header / impl is deprecated, and will be removed from Skia soon. +// +// Skia now has support for a 4x matrix (SkM44) in SkCanvas. +// + +class SkCanvas; + +// DEPRECATED +class SkPatch3D { +public: + SkPatch3D(); + + void reset(); + void transform(const SkM44&, SkPatch3D* dst = nullptr) const; + + // dot a unit vector with the patch's normal + SkScalar dotWith(SkScalar dx, SkScalar dy, SkScalar dz) const; + SkScalar dotWith(const SkV3& v) const { + return this->dotWith(v.x, v.y, v.z); + } + + // deprecated, but still here for animator (for now) + void rotate(SkScalar /*x*/, SkScalar /*y*/, SkScalar /*z*/) {} + void rotateDegrees(SkScalar /*x*/, SkScalar /*y*/, SkScalar /*z*/) {} + +private: +public: // make public for SkDraw3D for now + SkV3 fU, fV; + SkV3 fOrigin; + + friend class SkCamera3D; +}; + +// DEPRECATED +class SkCamera3D { +public: + SkCamera3D(); + + void reset(); + void update(); + void patchToMatrix(const SkPatch3D&, SkMatrix* matrix) const; + + SkV3 fLocation; // origin of the camera's space + SkV3 fAxis; // view direction + SkV3 fZenith; // up direction + SkV3 fObserver; // eye position (may not be the same as the origin) + +private: + mutable SkMatrix fOrientation; + mutable bool fNeedToUpdate; + + void doUpdate() const; +}; + +// DEPRECATED +class SK_API Sk3DView : SkNoncopyable { +public: + Sk3DView(); + ~Sk3DView(); + + void save(); + void restore(); + + void translate(SkScalar x, SkScalar y, SkScalar z); + void rotateX(SkScalar deg); + void rotateY(SkScalar deg); + void rotateZ(SkScalar deg); + +#ifdef SK_BUILD_FOR_ANDROID_FRAMEWORK + void setCameraLocation(SkScalar x, SkScalar y, SkScalar z); + SkScalar getCameraLocationX() const; + SkScalar getCameraLocationY() const; + SkScalar getCameraLocationZ() const; +#endif + + void getMatrix(SkMatrix*) const; + void applyToCanvas(SkCanvas*) const; + + SkScalar dotWithNormal(SkScalar dx, SkScalar dy, SkScalar dz) const; + +private: + struct Rec { + Rec* fNext; + SkM44 fMatrix; + }; + Rec* fRec; + Rec fInitialRec; + SkCamera3D fCamera; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkCanvasStateUtils.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkCanvasStateUtils.h new file mode 100644 index 00000000000..0172e379310 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkCanvasStateUtils.h @@ -0,0 +1,81 @@ +/* + * Copyright 2013 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkCanvasStateUtils_DEFINED +#define SkCanvasStateUtils_DEFINED + +#include "include/core/SkTypes.h" + +#include + +class SkCanvas; +class SkCanvasState; + +/** + * A set of functions that are useful for copying the state of an SkCanvas + * across a library boundary where the Skia library on the other side of the + * boundary may be newer. The expected usage is outline below... + * + * Lib Boundary + * CaptureCanvasState(...) ||| + * SkCanvas --> SkCanvasState ||| + * ||| CreateFromCanvasState(...) + * ||| SkCanvasState --> SkCanvas` + * ||| Draw into SkCanvas` + * ||| Unref SkCanvas` + * ReleaseCanvasState(...) ||| + * + */ +class SK_API SkCanvasStateUtils { +public: + /** + * Captures the current state of the canvas into an opaque ptr that is safe + * to pass to a different instance of Skia (which may be the same version, + * or may be newer). The function will return NULL in the event that one of the + * following conditions are true. + * 1) the canvas device type is not supported (currently only raster is supported) + * 2) the canvas clip type is not supported (currently only non-AA clips are supported) + * + * It is recommended that the original canvas also not be used until all + * canvases that have been created using its captured state have been dereferenced. + * + * Finally, it is important to note that any draw filters attached to the + * canvas are NOT currently captured. + * + * @param canvas The canvas you wish to capture the current state of. + * @return NULL or an opaque ptr that can be passed to CreateFromCanvasState + * to reconstruct the canvas. The caller is responsible for calling + * ReleaseCanvasState to free the memory associated with this state. + */ + static SkCanvasState* CaptureCanvasState(SkCanvas* canvas); + + /** + * Create a new SkCanvas from the captured state of another SkCanvas. The + * function will return NULL in the event that one of the + * following conditions are true. + * 1) the captured state is in an unrecognized format + * 2) the captured canvas device type is not supported + * + * @param state Opaque object created by CaptureCanvasState. + * @return NULL or an SkCanvas* whose devices and matrix/clip state are + * identical to the captured canvas. The caller is responsible for + * calling unref on the SkCanvas. + */ + static std::unique_ptr MakeFromCanvasState(const SkCanvasState* state); + + /** + * Free the memory associated with the captured canvas state. The state + * should not be released until all SkCanvas objects created using that + * state have been dereferenced. Must be called from the same library + * instance that created the state via CaptureCanvasState. + * + * @param state The captured state you wish to dispose of. + */ + static void ReleaseCanvasState(SkCanvasState* state); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkCustomTypeface.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkCustomTypeface.h new file mode 100644 index 00000000000..d387fb24ca6 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkCustomTypeface.h @@ -0,0 +1,69 @@ +/* + * Copyright 2020 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkCustomTypeface_DEFINED +#define SkCustomTypeface_DEFINED + +#include "include/core/SkDrawable.h" +#include "include/core/SkFontMetrics.h" +#include "include/core/SkFontStyle.h" +#include "include/core/SkPath.h" +#include "include/core/SkRect.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypeface.h" +#include "include/core/SkTypes.h" + +#include +#include + +class SkStream; +class SkStreamAsset; +struct SkFontArguments; + +class SK_API SkCustomTypefaceBuilder { +public: + SkCustomTypefaceBuilder(); + + void setGlyph(SkGlyphID, float advance, const SkPath&); + void setGlyph(SkGlyphID, float advance, sk_sp, const SkRect& bounds); + + void setMetrics(const SkFontMetrics& fm, float scale = 1); + void setFontStyle(SkFontStyle); + + sk_sp detach(); + + static constexpr SkTypeface::FactoryId FactoryId = SkSetFourByteTag('u','s','e','r'); + static sk_sp MakeFromStream(std::unique_ptr, const SkFontArguments&); + +private: + struct GlyphRec { + // logical union + SkPath fPath; + sk_sp fDrawable; + + SkRect fBounds = {0,0,0,0}; // only used for drawable glyphs atm + float fAdvance = 0; + + bool isDrawable() const { + SkASSERT(!fDrawable || fPath.isEmpty()); + return fDrawable != nullptr; + } + }; + + std::vector fGlyphRecs; + SkFontMetrics fMetrics; + SkFontStyle fStyle; + + GlyphRec& ensureStorage(SkGlyphID); + + static sk_sp Deserialize(SkStream*); + + friend class SkTypeface; + friend class SkUserTypeface; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkEventTracer.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkEventTracer.h new file mode 100644 index 00000000000..2ec0a3b3554 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkEventTracer.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2014 Google Inc. All rights reserved. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkEventTracer_DEFINED +#define SkEventTracer_DEFINED + +// The class in this header defines the interface between Skia's internal +// tracing macros and an external entity (e.g., Chrome) that will consume them. +// Such an entity should subclass SkEventTracer and provide an instance of +// that event to SkEventTracer::SetInstance. + +// If you're looking for the tracing macros to instrument Skia itself, those +// live in src/core/SkTraceEvent.h + +#include "include/core/SkTypes.h" + +#include + +class SK_API SkEventTracer { +public: + + typedef uint64_t Handle; + + /** + * If this is the first call to SetInstance or GetInstance then the passed instance is + * installed and true is returned. Otherwise, false is returned. In either case ownership of the + * tracer is transferred and it will be deleted when no longer needed. + * + * Not deleting the tracer on process exit should not cause problems as + * the whole heap is about to go away with the process. This can also + * improve performance by reducing the amount of work needed. + * + * @param leakTracer Do not delete tracer on process exit. + */ + static bool SetInstance(SkEventTracer*, bool leakTracer = false); + + /** + * Gets the event tracer. If this is the first call to SetInstance or GetIntance then a default + * event tracer is installed and returned. + */ + static SkEventTracer* GetInstance(); + + virtual ~SkEventTracer() = default; + + // The pointer returned from GetCategoryGroupEnabled() points to a + // value with zero or more of the following bits. Used in this class only. + // The TRACE_EVENT macros should only use the value as a bool. + // These values must be in sync with macro values in trace_event.h in chromium. + enum CategoryGroupEnabledFlags { + // Category group enabled for the recording mode. + kEnabledForRecording_CategoryGroupEnabledFlags = 1 << 0, + // Category group enabled for the monitoring mode. + kEnabledForMonitoring_CategoryGroupEnabledFlags = 1 << 1, + // Category group enabled by SetEventCallbackEnabled(). + kEnabledForEventCallback_CategoryGroupEnabledFlags = 1 << 2, + }; + + virtual const uint8_t* getCategoryGroupEnabled(const char* name) = 0; + virtual const char* getCategoryGroupName(const uint8_t* categoryEnabledFlag) = 0; + + virtual SkEventTracer::Handle + addTraceEvent(char phase, + const uint8_t* categoryEnabledFlag, + const char* name, + uint64_t id, + int32_t numArgs, + const char** argNames, + const uint8_t* argTypes, + const uint64_t* argValues, + uint8_t flags) = 0; + + virtual void + updateTraceEventDuration(const uint8_t* categoryEnabledFlag, + const char* name, + SkEventTracer::Handle handle) = 0; + + // Optional method that can be implemented to allow splitting up traces into different sections. + virtual void newTracingSection(const char*) {} + +protected: + SkEventTracer() = default; + SkEventTracer(const SkEventTracer&) = delete; + SkEventTracer& operator=(const SkEventTracer&) = delete; +}; + +#endif // SkEventTracer_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkNWayCanvas.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkNWayCanvas.h new file mode 100644 index 00000000000..0332a1432bb --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkNWayCanvas.h @@ -0,0 +1,123 @@ + +/* + * Copyright 2011 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkNWayCanvas_DEFINED +#define SkNWayCanvas_DEFINED + +#include "include/core/SkCanvasVirtualEnforcer.h" +#include "include/core/SkColor.h" +#include "include/core/SkM44.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkSamplingOptions.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkTDArray.h" +#include "include/utils/SkNoDrawCanvas.h" + +#include + +namespace sktext { +class GlyphRunList; +} + +class SkCanvas; +class SkData; +class SkDrawable; +class SkImage; +class SkMatrix; +class SkPaint; +class SkPath; +class SkPicture; +class SkRRect; +class SkRegion; +class SkShader; +class SkTextBlob; +class SkVertices; +enum class SkBlendMode; +enum class SkClipOp; +struct SkDrawShadowRec; +struct SkPoint; +struct SkRSXform; +struct SkRect; + +namespace sktext::gpu { class Slug; } + +class SK_API SkNWayCanvas : public SkCanvasVirtualEnforcer { +public: + SkNWayCanvas(int width, int height); + ~SkNWayCanvas() override; + + virtual void addCanvas(SkCanvas*); + virtual void removeCanvas(SkCanvas*); + virtual void removeAll(); + +protected: + SkTDArray fList; + + void willSave() override; + SaveLayerStrategy getSaveLayerStrategy(const SaveLayerRec&) override; + bool onDoSaveBehind(const SkRect*) override; + void willRestore() override; + + void didConcat44(const SkM44&) override; + void didSetM44(const SkM44&) override; + void didScale(SkScalar, SkScalar) override; + void didTranslate(SkScalar, SkScalar) override; + + void onDrawDRRect(const SkRRect&, const SkRRect&, const SkPaint&) override; + void onDrawGlyphRunList(const sktext::GlyphRunList&, const SkPaint&) override; + void onDrawTextBlob(const SkTextBlob* blob, SkScalar x, SkScalar y, + const SkPaint& paint) override; + void onDrawSlug(const sktext::gpu::Slug* slug, const SkPaint& paint) override; + void onDrawPatch(const SkPoint cubics[12], const SkColor colors[4], + const SkPoint texCoords[4], SkBlendMode, const SkPaint& paint) override; + + void onDrawPaint(const SkPaint&) override; + void onDrawBehind(const SkPaint&) override; + void onDrawPoints(PointMode, size_t count, const SkPoint pts[], const SkPaint&) override; + void onDrawRect(const SkRect&, const SkPaint&) override; + void onDrawRegion(const SkRegion&, const SkPaint&) override; + void onDrawOval(const SkRect&, const SkPaint&) override; + void onDrawArc(const SkRect&, SkScalar, SkScalar, bool, const SkPaint&) override; + void onDrawRRect(const SkRRect&, const SkPaint&) override; + void onDrawPath(const SkPath&, const SkPaint&) override; + + void onDrawImage2(const SkImage*, SkScalar, SkScalar, const SkSamplingOptions&, + const SkPaint*) override; + void onDrawImageRect2(const SkImage*, const SkRect&, const SkRect&, const SkSamplingOptions&, + const SkPaint*, SrcRectConstraint) override; + void onDrawImageLattice2(const SkImage*, const Lattice&, const SkRect&, SkFilterMode, + const SkPaint*) override; + void onDrawAtlas2(const SkImage*, const SkRSXform[], const SkRect[], const SkColor[], int, + SkBlendMode, const SkSamplingOptions&, const SkRect*, const SkPaint*) override; + + void onDrawVerticesObject(const SkVertices*, SkBlendMode, const SkPaint&) override; + void onDrawShadowRec(const SkPath&, const SkDrawShadowRec&) override; + + void onClipRect(const SkRect&, SkClipOp, ClipEdgeStyle) override; + void onClipRRect(const SkRRect&, SkClipOp, ClipEdgeStyle) override; + void onClipPath(const SkPath&, SkClipOp, ClipEdgeStyle) override; + void onClipShader(sk_sp, SkClipOp) override; + void onClipRegion(const SkRegion&, SkClipOp) override; + void onResetClip() override; + + void onDrawPicture(const SkPicture*, const SkMatrix*, const SkPaint*) override; + void onDrawDrawable(SkDrawable*, const SkMatrix*) override; + void onDrawAnnotation(const SkRect&, const char[], SkData*) override; + + void onDrawEdgeAAQuad(const SkRect&, const SkPoint[4], QuadAAFlags, const SkColor4f&, + SkBlendMode) override; + void onDrawEdgeAAImageSet2(const ImageSetEntry[], int count, const SkPoint[], const SkMatrix[], + const SkSamplingOptions&,const SkPaint*, SrcRectConstraint) override; + class Iter; +private: + using INHERITED = SkCanvasVirtualEnforcer; +}; + + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkNoDrawCanvas.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkNoDrawCanvas.h new file mode 100644 index 00000000000..bcf43bec3c8 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkNoDrawCanvas.h @@ -0,0 +1,78 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkNoDrawCanvas_DEFINED +#define SkNoDrawCanvas_DEFINED + +#include "include/core/SkCanvas.h" +#include "include/core/SkCanvasVirtualEnforcer.h" + +struct SkIRect; + +// SkNoDrawCanvas is a helper for SkCanvas subclasses which do not need to +// actually rasterize (e.g., analysis of the draw calls). +// +// It provides the following simplifications: +// +// * not backed by any device/pixels +// * conservative clipping (clipping calls only use rectangles) +// +class SK_API SkNoDrawCanvas : public SkCanvasVirtualEnforcer { +public: + SkNoDrawCanvas(int width, int height); + SkNoDrawCanvas(const SkIRect&); + + // Optimization to reset state to be the same as after construction. + void resetCanvas(int w, int h) { this->resetForNextPicture(SkIRect::MakeWH(w, h)); } + void resetCanvas(const SkIRect& rect) { this->resetForNextPicture(rect); } + +protected: + SaveLayerStrategy getSaveLayerStrategy(const SaveLayerRec& rec) override; + bool onDoSaveBehind(const SkRect*) override; + + // No-op overrides for aborting rasterization earlier than SkNullBlitter. + void onDrawAnnotation(const SkRect&, const char[], SkData*) override {} + void onDrawDRRect(const SkRRect&, const SkRRect&, const SkPaint&) override {} + void onDrawDrawable(SkDrawable*, const SkMatrix*) override {} + void onDrawTextBlob(const SkTextBlob*, SkScalar, SkScalar, const SkPaint&) override {} + void onDrawPatch(const SkPoint[12], const SkColor[4], const SkPoint[4], SkBlendMode, + const SkPaint&) override {} + + void onDrawPaint(const SkPaint&) override {} + void onDrawBehind(const SkPaint&) override {} + void onDrawPoints(PointMode, size_t, const SkPoint[], const SkPaint&) override {} + void onDrawRect(const SkRect&, const SkPaint&) override {} + void onDrawRegion(const SkRegion&, const SkPaint&) override {} + void onDrawOval(const SkRect&, const SkPaint&) override {} + void onDrawArc(const SkRect&, SkScalar, SkScalar, bool, const SkPaint&) override {} + void onDrawRRect(const SkRRect&, const SkPaint&) override {} + void onDrawPath(const SkPath&, const SkPaint&) override {} + + void onDrawImage2(const SkImage*, SkScalar, SkScalar, const SkSamplingOptions&, + const SkPaint*) override {} + void onDrawImageRect2(const SkImage*, const SkRect&, const SkRect&, const SkSamplingOptions&, + const SkPaint*, SrcRectConstraint) override {} + void onDrawImageLattice2(const SkImage*, const Lattice&, const SkRect&, SkFilterMode, + const SkPaint*) override {} + void onDrawAtlas2(const SkImage*, const SkRSXform[], const SkRect[], const SkColor[], int, + SkBlendMode, const SkSamplingOptions&, const SkRect*, const SkPaint*) override {} + + void onDrawVerticesObject(const SkVertices*, SkBlendMode, const SkPaint&) override {} + void onDrawShadowRec(const SkPath&, const SkDrawShadowRec&) override {} + void onDrawPicture(const SkPicture*, const SkMatrix*, const SkPaint*) override {} + + void onDrawEdgeAAQuad(const SkRect&, const SkPoint[4], QuadAAFlags, const SkColor4f&, + SkBlendMode) override {} + void onDrawEdgeAAImageSet2(const ImageSetEntry[], int, const SkPoint[], const SkMatrix[], + const SkSamplingOptions&, const SkPaint*, + SrcRectConstraint) override {} + +private: + using INHERITED = SkCanvasVirtualEnforcer; +}; + +#endif // SkNoDrawCanvas_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkNullCanvas.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkNullCanvas.h new file mode 100644 index 00000000000..a77e3e3de9b --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkNullCanvas.h @@ -0,0 +1,22 @@ +/* + * Copyright 2012 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkNullCanvas_DEFINED +#define SkNullCanvas_DEFINED + +#include "include/core/SkTypes.h" + +#include + +class SkCanvas; + +/** + * Creates a canvas that draws nothing. This is useful for performance testing. + */ +SK_API std::unique_ptr SkMakeNullCanvas(); + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkOrderedFontMgr.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkOrderedFontMgr.h new file mode 100644 index 00000000000..0b686e5edc5 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkOrderedFontMgr.h @@ -0,0 +1,66 @@ +/* + * Copyright 2021 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkOrderedFontMgr_DEFINED +#define SkOrderedFontMgr_DEFINED + +#include "include/core/SkFontMgr.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkTypes.h" + +#include +#include + +class SkData; +class SkFontStyle; +class SkStreamAsset; +class SkString; +class SkTypeface; +struct SkFontArguments; + +/** + * Collects an order list of other font managers, and visits them in order + * when a request to find or match is issued. + * + * Note: this explicitly fails on any attempt to Make a typeface: all of + * those requests will return null. + */ +class SK_API SkOrderedFontMgr : public SkFontMgr { +public: + SkOrderedFontMgr(); + ~SkOrderedFontMgr() override; + + void append(sk_sp); + +protected: + int onCountFamilies() const override; + void onGetFamilyName(int index, SkString* familyName) const override; + sk_sp onCreateStyleSet(int index)const override; + + sk_sp onMatchFamily(const char familyName[]) const override; + + sk_sp onMatchFamilyStyle(const char familyName[], + const SkFontStyle&) const override; + sk_sp onMatchFamilyStyleCharacter(const char familyName[], const SkFontStyle&, + const char* bcp47[], int bcp47Count, + SkUnichar character) const override; + + // Note: all of these always return null + sk_sp onMakeFromData(sk_sp, int ttcIndex) const override; + sk_sp onMakeFromStreamIndex(std::unique_ptr, + int ttcIndex) const override; + sk_sp onMakeFromStreamArgs(std::unique_ptr, + const SkFontArguments&) const override; + sk_sp onMakeFromFile(const char path[], int ttcIndex) const override; + + sk_sp onLegacyMakeTypeface(const char familyName[], SkFontStyle) const override; + +private: + std::vector> fList; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkPaintFilterCanvas.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkPaintFilterCanvas.h new file mode 100644 index 00000000000..ce86d2ea3a6 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkPaintFilterCanvas.h @@ -0,0 +1,140 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkPaintFilterCanvas_DEFINED +#define SkPaintFilterCanvas_DEFINED + +#include "include/core/SkCanvas.h" +#include "include/core/SkCanvasVirtualEnforcer.h" +#include "include/core/SkColor.h" +#include "include/core/SkImageInfo.h" +#include "include/core/SkRefCnt.h" +#include "include/core/SkSamplingOptions.h" +#include "include/core/SkScalar.h" +#include "include/core/SkSize.h" +#include "include/core/SkTypes.h" +#include "include/private/base/SkTDArray.h" +#include "include/utils/SkNWayCanvas.h" + +#include + +namespace sktext { +class GlyphRunList; +} + +class GrRecordingContext; +class SkData; +class SkDrawable; +class SkImage; +class SkMatrix; +class SkPaint; +class SkPath; +class SkPicture; +class SkPixmap; +class SkRRect; +class SkRegion; +class SkSurface; +class SkSurfaceProps; +class SkTextBlob; +class SkVertices; +enum class SkBlendMode; +struct SkDrawShadowRec; +struct SkPoint; +struct SkRSXform; +struct SkRect; + +/** \class SkPaintFilterCanvas + + A utility proxy base class for implementing draw/paint filters. +*/ +class SK_API SkPaintFilterCanvas : public SkCanvasVirtualEnforcer { +public: + /** + * The new SkPaintFilterCanvas is configured for forwarding to the + * specified canvas. Also copies the target canvas matrix and clip bounds. + */ + SkPaintFilterCanvas(SkCanvas* canvas); + + enum Type { + kPicture_Type, + }; + + // Forwarded to the wrapped canvas. + SkISize getBaseLayerSize() const override { return proxy()->getBaseLayerSize(); } + GrRecordingContext* recordingContext() const override { return proxy()->recordingContext(); } +protected: + /** + * Called with the paint that will be used to draw the specified type. + * The implementation may modify the paint as they wish. + * + * The result bool is used to determine whether the draw op is to be + * executed (true) or skipped (false). + * + * Note: The base implementation calls onFilter() for top-level/explicit paints only. + * To also filter encapsulated paints (e.g. SkPicture, SkTextBlob), clients may need to + * override the relevant methods (i.e. drawPicture, drawTextBlob). + */ + virtual bool onFilter(SkPaint& paint) const = 0; + + void onDrawPaint(const SkPaint&) override; + void onDrawBehind(const SkPaint&) override; + void onDrawPoints(PointMode, size_t count, const SkPoint pts[], const SkPaint&) override; + void onDrawRect(const SkRect&, const SkPaint&) override; + void onDrawRRect(const SkRRect&, const SkPaint&) override; + void onDrawDRRect(const SkRRect&, const SkRRect&, const SkPaint&) override; + void onDrawRegion(const SkRegion&, const SkPaint&) override; + void onDrawOval(const SkRect&, const SkPaint&) override; + void onDrawArc(const SkRect&, SkScalar, SkScalar, bool, const SkPaint&) override; + void onDrawPath(const SkPath&, const SkPaint&) override; + + void onDrawImage2(const SkImage*, SkScalar, SkScalar, const SkSamplingOptions&, + const SkPaint*) override; + void onDrawImageRect2(const SkImage*, const SkRect&, const SkRect&, const SkSamplingOptions&, + const SkPaint*, SrcRectConstraint) override; + void onDrawImageLattice2(const SkImage*, const Lattice&, const SkRect&, SkFilterMode, + const SkPaint*) override; + void onDrawAtlas2(const SkImage*, const SkRSXform[], const SkRect[], const SkColor[], int, + SkBlendMode, const SkSamplingOptions&, const SkRect*, const SkPaint*) override; + + void onDrawVerticesObject(const SkVertices*, SkBlendMode, const SkPaint&) override; + void onDrawPatch(const SkPoint cubics[12], const SkColor colors[4], + const SkPoint texCoords[4], SkBlendMode, + const SkPaint& paint) override; + void onDrawPicture(const SkPicture*, const SkMatrix*, const SkPaint*) override; + void onDrawDrawable(SkDrawable*, const SkMatrix*) override; + + void onDrawGlyphRunList(const sktext::GlyphRunList&, const SkPaint&) override; + void onDrawTextBlob(const SkTextBlob* blob, SkScalar x, SkScalar y, + const SkPaint& paint) override; + void onDrawAnnotation(const SkRect& rect, const char key[], SkData* value) override; + void onDrawShadowRec(const SkPath& path, const SkDrawShadowRec& rec) override; + + void onDrawEdgeAAQuad(const SkRect&, const SkPoint[4], QuadAAFlags, const SkColor4f&, + SkBlendMode) override; + void onDrawEdgeAAImageSet2(const ImageSetEntry[], int count, const SkPoint[], const SkMatrix[], + const SkSamplingOptions&,const SkPaint*, SrcRectConstraint) override; + + // Forwarded to the wrapped canvas. + sk_sp onNewSurface(const SkImageInfo&, const SkSurfaceProps&) override; + bool onPeekPixels(SkPixmap* pixmap) override; + bool onAccessTopLayerPixels(SkPixmap* pixmap) override; + SkImageInfo onImageInfo() const override; + bool onGetProps(SkSurfaceProps* props, bool top) const override; + +private: + class AutoPaintFilter; + + SkCanvas* proxy() const { SkASSERT(fList.size() == 1); return fList[0]; } + + SkPaintFilterCanvas* internal_private_asPaintFilterCanvas() const override { + return const_cast(this); + } + + friend class SkAndroidFrameworkUtils; +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkParse.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkParse.h new file mode 100644 index 00000000000..bcabc3c793a --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkParse.h @@ -0,0 +1,37 @@ + +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + + +#ifndef SkParse_DEFINED +#define SkParse_DEFINED + +#include "include/core/SkColor.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" + +#include +#include + +class SK_API SkParse { +public: + static int Count(const char str[]); // number of scalars or int values + static int Count(const char str[], char separator); + static const char* FindColor(const char str[], SkColor* value); + static const char* FindHex(const char str[], uint32_t* value); + static const char* FindMSec(const char str[], SkMSec* value); + static const char* FindNamedColor(const char str[], size_t len, SkColor* color); + static const char* FindS32(const char str[], int32_t* value); + static const char* FindScalar(const char str[], SkScalar* value); + static const char* FindScalars(const char str[], SkScalar value[], int count); + + static bool FindBool(const char str[], bool* value); + // return the index of str in list[], or -1 if not found + static int FindList(const char str[], const char list[]); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkParsePath.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkParsePath.h new file mode 100644 index 00000000000..acd0ef2305c --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkParsePath.h @@ -0,0 +1,25 @@ + +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + + +#ifndef SkParsePath_DEFINED +#define SkParsePath_DEFINED + +#include "include/core/SkPath.h" + +class SkString; + +class SK_API SkParsePath { +public: + static bool FromSVGString(const char str[], SkPath*); + + enum class PathEncoding { Absolute, Relative }; + static SkString ToSVGString(const SkPath&, PathEncoding = PathEncoding::Absolute); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkShadowUtils.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkShadowUtils.h new file mode 100644 index 00000000000..0f77b110962 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkShadowUtils.h @@ -0,0 +1,102 @@ + +/* + * Copyright 2017 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkShadowUtils_DEFINED +#define SkShadowUtils_DEFINED + +#include "include/core/SkColor.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" + +#include + +class SkCanvas; +class SkMatrix; +class SkPath; +struct SkPoint3; +struct SkRect; + +enum SkShadowFlags { + kNone_ShadowFlag = 0x00, + /** The occluding object is not opaque. Knowing that the occluder is opaque allows + * us to cull shadow geometry behind it and improve performance. */ + kTransparentOccluder_ShadowFlag = 0x01, + /** Don't try to use analytic shadows. */ + kGeometricOnly_ShadowFlag = 0x02, + /** Light position represents a direction, light radius is blur radius at elevation 1 */ + kDirectionalLight_ShadowFlag = 0x04, + /** Concave paths will only use blur to generate the shadow */ + kConcaveBlurOnly_ShadowFlag = 0x08, + /** mask for all shadow flags */ + kAll_ShadowFlag = 0x0F +}; + +class SK_API SkShadowUtils { +public: + /** + * Draw an offset spot shadow and outlining ambient shadow for the given path using a disc + * light. The shadow may be cached, depending on the path type and canvas matrix. If the + * matrix is perspective or the path is volatile, it will not be cached. + * + * @param canvas The canvas on which to draw the shadows. + * @param path The occluder used to generate the shadows. + * @param zPlaneParams Values for the plane function which returns the Z offset of the + * occluder from the canvas based on local x and y values (the current matrix is not applied). + * @param lightPos Generally, the 3D position of the light relative to the canvas plane. + * If kDirectionalLight_ShadowFlag is set, this specifies a vector pointing + * towards the light. + * @param lightRadius Generally, the radius of the disc light. + * If DirectionalLight_ShadowFlag is set, this specifies the amount of + * blur when the occluder is at Z offset == 1. The blur will grow linearly + * as the Z value increases. + * @param ambientColor The color of the ambient shadow. + * @param spotColor The color of the spot shadow. + * @param flags Options controlling opaque occluder optimizations, shadow appearance, + * and light position. See SkShadowFlags. + */ + static void DrawShadow(SkCanvas* canvas, const SkPath& path, const SkPoint3& zPlaneParams, + const SkPoint3& lightPos, SkScalar lightRadius, + SkColor ambientColor, SkColor spotColor, + uint32_t flags = SkShadowFlags::kNone_ShadowFlag); + + /** + * Generate bounding box for shadows relative to path. Includes both the ambient and spot + * shadow bounds. + * + * @param ctm Current transformation matrix to device space. + * @param path The occluder used to generate the shadows. + * @param zPlaneParams Values for the plane function which returns the Z offset of the + * occluder from the canvas based on local x and y values (the current matrix is not applied). + * @param lightPos Generally, the 3D position of the light relative to the canvas plane. + * If kDirectionalLight_ShadowFlag is set, this specifies a vector pointing + * towards the light. + * @param lightRadius Generally, the radius of the disc light. + * If DirectionalLight_ShadowFlag is set, this specifies the amount of + * blur when the occluder is at Z offset == 1. The blur will grow linearly + * as the Z value increases. + * @param flags Options controlling opaque occluder optimizations, shadow appearance, + * and light position. See SkShadowFlags. + * @param bounds Return value for shadow bounding box. + * @return Returns true if successful, false otherwise. + */ + static bool GetLocalBounds(const SkMatrix& ctm, const SkPath& path, + const SkPoint3& zPlaneParams, const SkPoint3& lightPos, + SkScalar lightRadius, uint32_t flags, SkRect* bounds); + + /** + * Helper routine to compute color values for one-pass tonal alpha. + * + * @param inAmbientColor Original ambient color + * @param inSpotColor Original spot color + * @param outAmbientColor Modified ambient color + * @param outSpotColor Modified spot color + */ + static void ComputeTonalColors(SkColor inAmbientColor, SkColor inSpotColor, + SkColor* outAmbientColor, SkColor* outSpotColor); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkTextUtils.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkTextUtils.h new file mode 100644 index 00000000000..06f83b934f9 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkTextUtils.h @@ -0,0 +1,43 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkTextUtils_DEFINED +#define SkTextUtils_DEFINED + +#include "include/core/SkFontTypes.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" + +#include +#include + +class SkCanvas; +class SkFont; +class SkPaint; +class SkPath; + +class SK_API SkTextUtils { +public: + enum Align { + kLeft_Align, + kCenter_Align, + kRight_Align, + }; + + static void Draw(SkCanvas*, const void* text, size_t size, SkTextEncoding, + SkScalar x, SkScalar y, const SkFont&, const SkPaint&, Align = kLeft_Align); + + static void DrawString(SkCanvas* canvas, const char text[], SkScalar x, SkScalar y, + const SkFont& font, const SkPaint& paint, Align align = kLeft_Align) { + Draw(canvas, text, strlen(text), SkTextEncoding::kUTF8, x, y, font, paint, align); + } + + static void GetPath(const void* text, size_t length, SkTextEncoding, SkScalar x, SkScalar y, + const SkFont&, SkPath*); +}; + +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkTraceEventPhase.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkTraceEventPhase.h new file mode 100644 index 00000000000..38457be24b4 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/SkTraceEventPhase.h @@ -0,0 +1,19 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#ifndef SkTraceEventPhase_DEFINED +#define SkTraceEventPhase_DEFINED + +// Phase indicates the nature of an event entry. E.g. part of a begin/end pair. +#define TRACE_EVENT_PHASE_BEGIN ('B') +#define TRACE_EVENT_PHASE_END ('E') +#define TRACE_EVENT_PHASE_COMPLETE ('X') +#define TRACE_EVENT_PHASE_INSTANT ('I') +#define TRACE_EVENT_PHASE_ASYNC_BEGIN ('S') +#define TRACE_EVENT_PHASE_ASYNC_END ('F') +#define TRACE_EVENT_PHASE_COUNTER ('C') +#define TRACE_EVENT_PHASE_CREATE_OBJECT ('N') +#define TRACE_EVENT_PHASE_SNAPSHOT_OBJECT ('O') +#define TRACE_EVENT_PHASE_DELETE_OBJECT ('D') + +#endif // SkTraceEventPhase_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/mac/SkCGUtils.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/mac/SkCGUtils.h new file mode 100644 index 00000000000..73d89c174ad --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/include/utils/mac/SkCGUtils.h @@ -0,0 +1,90 @@ + +/* + * Copyright 2011 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#ifndef SkCGUtils_DEFINED +#define SkCGUtils_DEFINED + +#include "include/core/SkImage.h" +#include "include/core/SkImageInfo.h" +#include "include/core/SkPixmap.h" +#include "include/core/SkSize.h" + +#if defined(SK_BUILD_FOR_MAC) || defined(SK_BUILD_FOR_IOS) + +#ifdef SK_BUILD_FOR_MAC +#include +#endif + +#ifdef SK_BUILD_FOR_IOS +#include +#endif + +class SkBitmap; +class SkColorSpace; +class SkData; +class SkPixmap; +class SkStreamRewindable; + +SK_API CGContextRef SkCreateCGContext(const SkPixmap&); + +/** + * Given a CGImage, allocate an SkBitmap and copy the image's pixels into it. If scaleToFit is not + * null, use it to determine the size of the bitmap, and scale the image to fill the bitmap. + * Otherwise use the image's width/height. + * + * On failure, return false, and leave bitmap unchanged. + */ +SK_API bool SkCreateBitmapFromCGImage(SkBitmap* dst, CGImageRef src); + +SK_API sk_sp SkMakeImageFromCGImage(CGImageRef); + +/** + * Given a CGColorSpace, return the closest matching SkColorSpace. If no conversion is possible + * or if the input CGColorSpace is nullptr then return nullptr. + */ +SK_API sk_sp SkMakeColorSpaceFromCGColorSpace(CGColorSpaceRef); + +/** + * Copy the pixels from src into the memory specified by info/rowBytes/dstPixels. On failure, + * return false (e.g. ImageInfo incompatible with src). + */ +SK_API bool SkCopyPixelsFromCGImage(const SkImageInfo& info, size_t rowBytes, void* dstPixels, + CGImageRef src); +static inline bool SkCopyPixelsFromCGImage(const SkPixmap& dst, CGImageRef src) { + return SkCopyPixelsFromCGImage(dst.info(), dst.rowBytes(), dst.writable_addr(), src); +} + +/** + * Create an imageref from the specified bitmap. The color space parameter is ignored. + */ +SK_API CGImageRef SkCreateCGImageRefWithColorspace(const SkBitmap& bm, + CGColorSpaceRef space); + +/** + * Create an imageref from the specified bitmap. + */ +SK_API CGImageRef SkCreateCGImageRef(const SkBitmap& bm); + +/** + * Given an SkColorSpace, create a CGColorSpace. This will return sRGB if the specified + * SkColorSpace is nullptr or on failure. This will not retain the specified SkColorSpace. + */ +SK_API CGColorSpaceRef SkCreateCGColorSpace(const SkColorSpace*); + +/** + * Given an SkData, create a CGDataProviderRef that refers to the and retains the specified data. + */ +SK_API CGDataProviderRef SkCreateCGDataProvider(sk_sp); + +/** + * Draw the bitmap into the specified CG context. (x,y) specifies the position of the top-left + * corner of the bitmap. + */ +void SkCGDrawBitmap(CGContextRef, const SkBitmap&, float x, float y); + +#endif // defined(SK_BUILD_FOR_MAC) || defined(SK_BUILD_FOR_IOS) +#endif // SkCGUtils_DEFINED diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/BUILD.gn b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/BUILD.gn new file mode 100644 index 00000000000..5d037a94c93 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/BUILD.gn @@ -0,0 +1,100 @@ +# Copyright 2022 Google LLC +# +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("skcms.gni") + +# Use for CPU-specific skcms transform code that needs particular compiler flags. +# (This is patterned after `opts` in Skia's BUILD.gn.) +template("arch") { + if (invoker.enabled) { + source_set(target_name) { + visibility = [ ":*" ] + check_includes = false + forward_variables_from(invoker, "*") + } + } else { + # If not enabled, a phony empty target that swallows all otherwise unused variables. + source_set(target_name) { + visibility = [ ":*" ] + check_includes = false + forward_variables_from(invoker, + "*", + [ + "sources", + "cflags", + "defines", + ]) + } + } +} + +arch("skcms_TransformHsw") { + enabled = current_cpu == "x64" && target_os != "android" + sources = skcms_TransformHsw + if (is_win) { + if (is_clang) { + cflags = [ + "/clang:-mavx2", + "/clang:-mf16c", + "/clang:-ffp-contract=off", + ] + } else { + cflags = [ "/arch:AVX2" ] + } + } else { + cflags = [ + "-mavx2", + "-mf16c", + "-std=c11", + ] + } +} + +arch("skcms_TransformSkx") { + enabled = current_cpu == "x64" && target_os != "android" + sources = skcms_TransformSkx + if (is_win) { + if (is_clang) { + cflags = [ + "/clang:-mavx512f", + "/clang:-mavx512dq", + "/clang:-mavx512cd", + "/clang:-mavx512bw", + "/clang:-mavx512vl", + "/clang:-ffp-contract=off", + ] + } else { + cflags = [ "/arch:AVX512" ] + } + } else { + cflags = [ + "-mavx512f", + "-mavx512dq", + "-mavx512cd", + "-mavx512bw", + "-mavx512vl", + "-std=c11", + ] + } +} + +static_library("skcms") { + cflags = [] + if (!is_win || is_clang) { + cflags += [ "-std=c11" ] + } + if (target_cpu != "x64" || target_os == "android") { + defines = [ + "SKCMS_DISABLE_HSW", + "SKCMS_DISABLE_SKX", + ] + } + public = skcms_public_headers + sources = skcms_public + skcms_TransformBaseline + deps = [ + ":skcms_TransformHsw", + ":skcms_TransformSkx", + ] +} diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/OWNERS b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/OWNERS new file mode 100644 index 00000000000..cc36d27e3d1 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/OWNERS @@ -0,0 +1,2 @@ +# The auto-roller directly checks in skcms, so give it ownership as well: +skia-autoroll@skia-public.iam.gserviceaccount.com diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/README.chromium b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/README.chromium new file mode 100644 index 00000000000..15543c64fd5 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/README.chromium @@ -0,0 +1,6 @@ +Name: skcms +URL: https://skia.org/ +Version: unknown +Security Critical: yes +Shipped: yes +License: BSD diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/skcms.cc b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/skcms.cc new file mode 100644 index 00000000000..047f21bed1a --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/skcms.cc @@ -0,0 +1,2889 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "src/skcms_public.h" // NO_G3_REWRITE +#include "src/skcms_internals.h" // NO_G3_REWRITE +#include "src/skcms_Transform.h" // NO_G3_REWRITE +#include +#include +#include +#include +#include + +#if defined(__ARM_NEON) + #include +#elif defined(__SSE__) + #include + + #if defined(__clang__) + // That #include is usually enough, but Clang's headers + // "helpfully" skip including the whole kitchen sink when _MSC_VER is + // defined, because lots of programs on Windows would include that and + // it'd be a lot slower. But we want all those headers included so we + // can use their features after runtime checks later. + #include + #include + #include + #include + #include + #endif +#endif + +using namespace skcms_private; + +static bool sAllowRuntimeCPUDetection = true; + +void skcms_DisableRuntimeCPUDetection() { + sAllowRuntimeCPUDetection = false; +} + +static float log2f_(float x) { + // The first approximation of log2(x) is its exponent 'e', minus 127. + int32_t bits; + memcpy(&bits, &x, sizeof(bits)); + + float e = (float)bits * (1.0f / (1<<23)); + + // If we use the mantissa too we can refine the error signficantly. + int32_t m_bits = (bits & 0x007fffff) | 0x3f000000; + float m; + memcpy(&m, &m_bits, sizeof(m)); + + return (e - 124.225514990f + - 1.498030302f*m + - 1.725879990f/(0.3520887068f + m)); +} +static float logf_(float x) { + const float ln2 = 0.69314718f; + return ln2*log2f_(x); +} + +static float exp2f_(float x) { + if (x > 128.0f) { + return INFINITY_; + } else if (x < -127.0f) { + return 0.0f; + } + float fract = x - floorf_(x); + + float fbits = (1.0f * (1<<23)) * (x + 121.274057500f + - 1.490129070f*fract + + 27.728023300f/(4.84252568f - fract)); + + // Before we cast fbits to int32_t, check for out of range values to pacify UBSAN. + // INT_MAX is not exactly representable as a float, so exclude it as effectively infinite. + // Negative values are effectively underflow - we'll end up returning a (different) negative + // value, which makes no sense. So clamp to zero. + if (fbits >= (float)INT_MAX) { + return INFINITY_; + } else if (fbits < 0) { + return 0; + } + + int32_t bits = (int32_t)fbits; + memcpy(&x, &bits, sizeof(x)); + return x; +} + +// Not static, as it's used by some test tools. +float powf_(float x, float y) { + if (x <= 0.f) { + return 0.f; + } + if (x == 1.f) { + return 1.f; + } + return exp2f_(log2f_(x) * y); +} + +static float expf_(float x) { + const float log2_e = 1.4426950408889634074f; + return exp2f_(log2_e * x); +} + +static float fmaxf_(float x, float y) { return x > y ? x : y; } +static float fminf_(float x, float y) { return x < y ? x : y; } + +static bool isfinitef_(float x) { return 0 == x*0; } + +static float minus_1_ulp(float x) { + int32_t bits; + memcpy(&bits, &x, sizeof(bits)); + bits = bits - 1; + memcpy(&x, &bits, sizeof(bits)); + return x; +} + +// Most transfer functions we work with are sRGBish. +// For exotic HDR transfer functions, we encode them using a tf.g that makes no sense, +// and repurpose the other fields to hold the parameters of the HDR functions. +struct TF_PQish { float A,B,C,D,E,F; }; +struct TF_HLGish { float R,G,a,b,c,K_minus_1; }; +// We didn't originally support a scale factor K for HLG, and instead just stored 0 in +// the unused `f` field of skcms_TransferFunction for HLGish and HLGInvish transfer functions. +// By storing f=K-1, those old unusued f=0 values now mean K=1, a noop scale factor. + +static float TFKind_marker(skcms_TFType kind) { + // We'd use different NaNs, but those aren't guaranteed to be preserved by WASM. + return -(float)kind; +} + +static skcms_TFType classify(const skcms_TransferFunction& tf, TF_PQish* pq = nullptr + , TF_HLGish* hlg = nullptr) { + if (tf.g < 0) { + // Negative "g" is mapped to enum values; large negative are for sure invalid. + if (tf.g < -128) { + return skcms_TFType_Invalid; + } + int enum_g = -static_cast(tf.g); + // Non-whole "g" values are invalid as well. + if (static_cast(-enum_g) != tf.g) { + return skcms_TFType_Invalid; + } + // TODO: soundness checks for PQ/HLG like we do for sRGBish? + switch (enum_g) { + case skcms_TFType_PQish: + if (pq) { + memcpy(pq , &tf.a, sizeof(*pq )); + } + return skcms_TFType_PQish; + case skcms_TFType_HLGish: + if (hlg) { + memcpy(hlg, &tf.a, sizeof(*hlg)); + } + return skcms_TFType_HLGish; + case skcms_TFType_HLGinvish: + if (hlg) { + memcpy(hlg, &tf.a, sizeof(*hlg)); + } + return skcms_TFType_HLGinvish; + } + return skcms_TFType_Invalid; + } + + // Basic soundness checks for sRGBish transfer functions. + if (isfinitef_(tf.a + tf.b + tf.c + tf.d + tf.e + tf.f + tf.g) + // a,c,d,g should be non-negative to make any sense. + && tf.a >= 0 + && tf.c >= 0 + && tf.d >= 0 + && tf.g >= 0 + // Raising a negative value to a fractional tf->g produces complex numbers. + && tf.a * tf.d + tf.b >= 0) { + return skcms_TFType_sRGBish; + } + + return skcms_TFType_Invalid; +} + +skcms_TFType skcms_TransferFunction_getType(const skcms_TransferFunction* tf) { + return classify(*tf); +} +bool skcms_TransferFunction_isSRGBish(const skcms_TransferFunction* tf) { + return classify(*tf) == skcms_TFType_sRGBish; +} +bool skcms_TransferFunction_isPQish(const skcms_TransferFunction* tf) { + return classify(*tf) == skcms_TFType_PQish; +} +bool skcms_TransferFunction_isHLGish(const skcms_TransferFunction* tf) { + return classify(*tf) == skcms_TFType_HLGish; +} + +bool skcms_TransferFunction_makePQish(skcms_TransferFunction* tf, + float A, float B, float C, + float D, float E, float F) { + *tf = { TFKind_marker(skcms_TFType_PQish), A,B,C,D,E,F }; + assert(skcms_TransferFunction_isPQish(tf)); + return true; +} + +bool skcms_TransferFunction_makeScaledHLGish(skcms_TransferFunction* tf, + float K, float R, float G, + float a, float b, float c) { + *tf = { TFKind_marker(skcms_TFType_HLGish), R,G, a,b,c, K-1.0f }; + assert(skcms_TransferFunction_isHLGish(tf)); + return true; +} + +float skcms_TransferFunction_eval(const skcms_TransferFunction* tf, float x) { + float sign = x < 0 ? -1.0f : 1.0f; + x *= sign; + + TF_PQish pq; + TF_HLGish hlg; + switch (classify(*tf, &pq, &hlg)) { + case skcms_TFType_Invalid: break; + + case skcms_TFType_HLGish: { + const float K = hlg.K_minus_1 + 1.0f; + return K * sign * (x*hlg.R <= 1 ? powf_(x*hlg.R, hlg.G) + : expf_((x-hlg.c)*hlg.a) + hlg.b); + } + + // skcms_TransferFunction_invert() inverts R, G, and a for HLGinvish so this math is fast. + case skcms_TFType_HLGinvish: { + const float K = hlg.K_minus_1 + 1.0f; + x /= K; + return sign * (x <= 1 ? hlg.R * powf_(x, hlg.G) + : hlg.a * logf_(x - hlg.b) + hlg.c); + } + + case skcms_TFType_sRGBish: + return sign * (x < tf->d ? tf->c * x + tf->f + : powf_(tf->a * x + tf->b, tf->g) + tf->e); + + case skcms_TFType_PQish: + return sign * + powf_((pq.A + pq.B * powf_(x, pq.C)) / (pq.D + pq.E * powf_(x, pq.C)), pq.F); + } + return 0; +} + + +static float eval_curve(const skcms_Curve* curve, float x) { + if (curve->table_entries == 0) { + return skcms_TransferFunction_eval(&curve->parametric, x); + } + + float ix = fmaxf_(0, fminf_(x, 1)) * static_cast(curve->table_entries - 1); + int lo = (int) ix , + hi = (int)(float)minus_1_ulp(ix + 1.0f); + float t = ix - (float)lo; + + float l, h; + if (curve->table_8) { + l = curve->table_8[lo] * (1/255.0f); + h = curve->table_8[hi] * (1/255.0f); + } else { + uint16_t be_l, be_h; + memcpy(&be_l, curve->table_16 + 2*lo, 2); + memcpy(&be_h, curve->table_16 + 2*hi, 2); + uint16_t le_l = ((be_l << 8) | (be_l >> 8)) & 0xffff; + uint16_t le_h = ((be_h << 8) | (be_h >> 8)) & 0xffff; + l = le_l * (1/65535.0f); + h = le_h * (1/65535.0f); + } + return l + (h-l)*t; +} + +float skcms_MaxRoundtripError(const skcms_Curve* curve, const skcms_TransferFunction* inv_tf) { + uint32_t N = curve->table_entries > 256 ? curve->table_entries : 256; + const float dx = 1.0f / static_cast(N - 1); + float err = 0; + for (uint32_t i = 0; i < N; i++) { + float x = static_cast(i) * dx, + y = eval_curve(curve, x); + err = fmaxf_(err, fabsf_(x - skcms_TransferFunction_eval(inv_tf, y))); + } + return err; +} + +bool skcms_AreApproximateInverses(const skcms_Curve* curve, const skcms_TransferFunction* inv_tf) { + return skcms_MaxRoundtripError(curve, inv_tf) < (1/512.0f); +} + +// Additional ICC signature values that are only used internally +enum { + // File signature + skcms_Signature_acsp = 0x61637370, + + // Tag signatures + skcms_Signature_rTRC = 0x72545243, + skcms_Signature_gTRC = 0x67545243, + skcms_Signature_bTRC = 0x62545243, + skcms_Signature_kTRC = 0x6B545243, + + skcms_Signature_rXYZ = 0x7258595A, + skcms_Signature_gXYZ = 0x6758595A, + skcms_Signature_bXYZ = 0x6258595A, + + skcms_Signature_A2B0 = 0x41324230, + skcms_Signature_B2A0 = 0x42324130, + + skcms_Signature_CHAD = 0x63686164, + skcms_Signature_WTPT = 0x77747074, + + skcms_Signature_CICP = 0x63696370, + + // Type signatures + skcms_Signature_curv = 0x63757276, + skcms_Signature_mft1 = 0x6D667431, + skcms_Signature_mft2 = 0x6D667432, + skcms_Signature_mAB = 0x6D414220, + skcms_Signature_mBA = 0x6D424120, + skcms_Signature_para = 0x70617261, + skcms_Signature_sf32 = 0x73663332, + // XYZ is also a PCS signature, so it's defined in skcms.h + // skcms_Signature_XYZ = 0x58595A20, +}; + +static uint16_t read_big_u16(const uint8_t* ptr) { + uint16_t be; + memcpy(&be, ptr, sizeof(be)); +#if defined(_MSC_VER) + return _byteswap_ushort(be); +#else + return __builtin_bswap16(be); +#endif +} + +static uint32_t read_big_u32(const uint8_t* ptr) { + uint32_t be; + memcpy(&be, ptr, sizeof(be)); +#if defined(_MSC_VER) + return _byteswap_ulong(be); +#else + return __builtin_bswap32(be); +#endif +} + +static int32_t read_big_i32(const uint8_t* ptr) { + return (int32_t)read_big_u32(ptr); +} + +static float read_big_fixed(const uint8_t* ptr) { + return static_cast(read_big_i32(ptr)) * (1.0f / 65536.0f); +} + +// Maps to an in-memory profile so that fields line up to the locations specified +// in ICC.1:2010, section 7.2 +typedef struct { + uint8_t size [ 4]; + uint8_t cmm_type [ 4]; + uint8_t version [ 4]; + uint8_t profile_class [ 4]; + uint8_t data_color_space [ 4]; + uint8_t pcs [ 4]; + uint8_t creation_date_time [12]; + uint8_t signature [ 4]; + uint8_t platform [ 4]; + uint8_t flags [ 4]; + uint8_t device_manufacturer [ 4]; + uint8_t device_model [ 4]; + uint8_t device_attributes [ 8]; + uint8_t rendering_intent [ 4]; + uint8_t illuminant_X [ 4]; + uint8_t illuminant_Y [ 4]; + uint8_t illuminant_Z [ 4]; + uint8_t creator [ 4]; + uint8_t profile_id [16]; + uint8_t reserved [28]; + uint8_t tag_count [ 4]; // Technically not part of header, but required +} header_Layout; + +typedef struct { + uint8_t signature [4]; + uint8_t offset [4]; + uint8_t size [4]; +} tag_Layout; + +static const tag_Layout* get_tag_table(const skcms_ICCProfile* profile) { + return (const tag_Layout*)(profile->buffer + SAFE_SIZEOF(header_Layout)); +} + +// s15Fixed16ArrayType is technically variable sized, holding N values. However, the only valid +// use of the type is for the CHAD tag that stores exactly nine values. +typedef struct { + uint8_t type [ 4]; + uint8_t reserved [ 4]; + uint8_t values [36]; +} sf32_Layout; + +bool skcms_GetCHAD(const skcms_ICCProfile* profile, skcms_Matrix3x3* m) { + skcms_ICCTag tag; + if (!skcms_GetTagBySignature(profile, skcms_Signature_CHAD, &tag)) { + return false; + } + + if (tag.type != skcms_Signature_sf32 || tag.size < SAFE_SIZEOF(sf32_Layout)) { + return false; + } + + const sf32_Layout* sf32Tag = (const sf32_Layout*)tag.buf; + const uint8_t* values = sf32Tag->values; + for (int r = 0; r < 3; ++r) + for (int c = 0; c < 3; ++c, values += 4) { + m->vals[r][c] = read_big_fixed(values); + } + return true; +} + +// XYZType is technically variable sized, holding N XYZ triples. However, the only valid uses of +// the type are for tags/data that store exactly one triple. +typedef struct { + uint8_t type [4]; + uint8_t reserved [4]; + uint8_t X [4]; + uint8_t Y [4]; + uint8_t Z [4]; +} XYZ_Layout; + +static bool read_tag_xyz(const skcms_ICCTag* tag, float* x, float* y, float* z) { + if (tag->type != skcms_Signature_XYZ || tag->size < SAFE_SIZEOF(XYZ_Layout)) { + return false; + } + + const XYZ_Layout* xyzTag = (const XYZ_Layout*)tag->buf; + + *x = read_big_fixed(xyzTag->X); + *y = read_big_fixed(xyzTag->Y); + *z = read_big_fixed(xyzTag->Z); + return true; +} + +bool skcms_GetWTPT(const skcms_ICCProfile* profile, float xyz[3]) { + skcms_ICCTag tag; + return skcms_GetTagBySignature(profile, skcms_Signature_WTPT, &tag) && + read_tag_xyz(&tag, &xyz[0], &xyz[1], &xyz[2]); +} + +static bool read_to_XYZD50(const skcms_ICCTag* rXYZ, const skcms_ICCTag* gXYZ, + const skcms_ICCTag* bXYZ, skcms_Matrix3x3* toXYZ) { + return read_tag_xyz(rXYZ, &toXYZ->vals[0][0], &toXYZ->vals[1][0], &toXYZ->vals[2][0]) && + read_tag_xyz(gXYZ, &toXYZ->vals[0][1], &toXYZ->vals[1][1], &toXYZ->vals[2][1]) && + read_tag_xyz(bXYZ, &toXYZ->vals[0][2], &toXYZ->vals[1][2], &toXYZ->vals[2][2]); +} + +typedef struct { + uint8_t type [4]; + uint8_t reserved_a [4]; + uint8_t function_type [2]; + uint8_t reserved_b [2]; + uint8_t variable [1/*variable*/]; // 1, 3, 4, 5, or 7 s15.16, depending on function_type +} para_Layout; + +static bool read_curve_para(const uint8_t* buf, uint32_t size, + skcms_Curve* curve, uint32_t* curve_size) { + if (size < SAFE_FIXED_SIZE(para_Layout)) { + return false; + } + + const para_Layout* paraTag = (const para_Layout*)buf; + + enum { kG = 0, kGAB = 1, kGABC = 2, kGABCD = 3, kGABCDEF = 4 }; + uint16_t function_type = read_big_u16(paraTag->function_type); + if (function_type > kGABCDEF) { + return false; + } + + static const uint32_t curve_bytes[] = { 4, 12, 16, 20, 28 }; + if (size < SAFE_FIXED_SIZE(para_Layout) + curve_bytes[function_type]) { + return false; + } + + if (curve_size) { + *curve_size = SAFE_FIXED_SIZE(para_Layout) + curve_bytes[function_type]; + } + + curve->table_entries = 0; + curve->parametric.a = 1.0f; + curve->parametric.b = 0.0f; + curve->parametric.c = 0.0f; + curve->parametric.d = 0.0f; + curve->parametric.e = 0.0f; + curve->parametric.f = 0.0f; + curve->parametric.g = read_big_fixed(paraTag->variable); + + switch (function_type) { + case kGAB: + curve->parametric.a = read_big_fixed(paraTag->variable + 4); + curve->parametric.b = read_big_fixed(paraTag->variable + 8); + if (curve->parametric.a == 0) { + return false; + } + curve->parametric.d = -curve->parametric.b / curve->parametric.a; + break; + case kGABC: + curve->parametric.a = read_big_fixed(paraTag->variable + 4); + curve->parametric.b = read_big_fixed(paraTag->variable + 8); + curve->parametric.e = read_big_fixed(paraTag->variable + 12); + if (curve->parametric.a == 0) { + return false; + } + curve->parametric.d = -curve->parametric.b / curve->parametric.a; + curve->parametric.f = curve->parametric.e; + break; + case kGABCD: + curve->parametric.a = read_big_fixed(paraTag->variable + 4); + curve->parametric.b = read_big_fixed(paraTag->variable + 8); + curve->parametric.c = read_big_fixed(paraTag->variable + 12); + curve->parametric.d = read_big_fixed(paraTag->variable + 16); + break; + case kGABCDEF: + curve->parametric.a = read_big_fixed(paraTag->variable + 4); + curve->parametric.b = read_big_fixed(paraTag->variable + 8); + curve->parametric.c = read_big_fixed(paraTag->variable + 12); + curve->parametric.d = read_big_fixed(paraTag->variable + 16); + curve->parametric.e = read_big_fixed(paraTag->variable + 20); + curve->parametric.f = read_big_fixed(paraTag->variable + 24); + break; + } + return skcms_TransferFunction_isSRGBish(&curve->parametric); +} + +typedef struct { + uint8_t type [4]; + uint8_t reserved [4]; + uint8_t value_count [4]; + uint8_t variable [1/*variable*/]; // value_count, 8.8 if 1, uint16 (n*65535) if > 1 +} curv_Layout; + +static bool read_curve_curv(const uint8_t* buf, uint32_t size, + skcms_Curve* curve, uint32_t* curve_size) { + if (size < SAFE_FIXED_SIZE(curv_Layout)) { + return false; + } + + const curv_Layout* curvTag = (const curv_Layout*)buf; + + uint32_t value_count = read_big_u32(curvTag->value_count); + if (size < SAFE_FIXED_SIZE(curv_Layout) + value_count * SAFE_SIZEOF(uint16_t)) { + return false; + } + + if (curve_size) { + *curve_size = SAFE_FIXED_SIZE(curv_Layout) + value_count * SAFE_SIZEOF(uint16_t); + } + + if (value_count < 2) { + curve->table_entries = 0; + curve->parametric.a = 1.0f; + curve->parametric.b = 0.0f; + curve->parametric.c = 0.0f; + curve->parametric.d = 0.0f; + curve->parametric.e = 0.0f; + curve->parametric.f = 0.0f; + if (value_count == 0) { + // Empty tables are a shorthand for an identity curve + curve->parametric.g = 1.0f; + } else { + // Single entry tables are a shorthand for simple gamma + curve->parametric.g = read_big_u16(curvTag->variable) * (1.0f / 256.0f); + } + } else { + curve->table_8 = nullptr; + curve->table_16 = curvTag->variable; + curve->table_entries = value_count; + } + + return true; +} + +// Parses both curveType and parametricCurveType data. Ensures that at most 'size' bytes are read. +// If curve_size is not nullptr, writes the number of bytes used by the curve in (*curve_size). +static bool read_curve(const uint8_t* buf, uint32_t size, + skcms_Curve* curve, uint32_t* curve_size) { + if (!buf || size < 4 || !curve) { + return false; + } + + uint32_t type = read_big_u32(buf); + if (type == skcms_Signature_para) { + return read_curve_para(buf, size, curve, curve_size); + } else if (type == skcms_Signature_curv) { + return read_curve_curv(buf, size, curve, curve_size); + } + + return false; +} + +// mft1 and mft2 share a large chunk of data +typedef struct { + uint8_t type [ 4]; + uint8_t reserved_a [ 4]; + uint8_t input_channels [ 1]; + uint8_t output_channels [ 1]; + uint8_t grid_points [ 1]; + uint8_t reserved_b [ 1]; + uint8_t matrix [36]; +} mft_CommonLayout; + +typedef struct { + mft_CommonLayout common [1]; + + uint8_t variable [1/*variable*/]; +} mft1_Layout; + +typedef struct { + mft_CommonLayout common [1]; + + uint8_t input_table_entries [2]; + uint8_t output_table_entries [2]; + uint8_t variable [1/*variable*/]; +} mft2_Layout; + +static bool read_mft_common(const mft_CommonLayout* mftTag, skcms_A2B* a2b) { + // MFT matrices are applied before the first set of curves, but must be identity unless the + // input is PCSXYZ. We don't support PCSXYZ profiles, so we ignore this matrix. Note that the + // matrix in skcms_A2B is applied later in the pipe, so supporting this would require another + // field/flag. + a2b->matrix_channels = 0; + a2b-> input_channels = mftTag-> input_channels[0]; + a2b->output_channels = mftTag->output_channels[0]; + + // We require exactly three (ie XYZ/Lab/RGB) output channels + if (a2b->output_channels != ARRAY_COUNT(a2b->output_curves)) { + return false; + } + // We require at least one, and no more than four (ie CMYK) input channels + if (a2b->input_channels < 1 || a2b->input_channels > ARRAY_COUNT(a2b->input_curves)) { + return false; + } + + for (uint32_t i = 0; i < a2b->input_channels; ++i) { + a2b->grid_points[i] = mftTag->grid_points[0]; + } + // The grid only makes sense with at least two points along each axis + if (a2b->grid_points[0] < 2) { + return false; + } + return true; +} + +// All as the A2B version above, except where noted. +static bool read_mft_common(const mft_CommonLayout* mftTag, skcms_B2A* b2a) { + // Same as A2B. + b2a->matrix_channels = 0; + b2a-> input_channels = mftTag-> input_channels[0]; + b2a->output_channels = mftTag->output_channels[0]; + + + // For B2A, exactly 3 input channels (XYZ) and 3 (RGB) or 4 (CMYK) output channels. + if (b2a->input_channels != ARRAY_COUNT(b2a->input_curves)) { + return false; + } + if (b2a->output_channels < 3 || b2a->output_channels > ARRAY_COUNT(b2a->output_curves)) { + return false; + } + + // Same as A2B. + for (uint32_t i = 0; i < b2a->input_channels; ++i) { + b2a->grid_points[i] = mftTag->grid_points[0]; + } + if (b2a->grid_points[0] < 2) { + return false; + } + return true; +} + +template +static bool init_tables(const uint8_t* table_base, uint64_t max_tables_len, uint32_t byte_width, + uint32_t input_table_entries, uint32_t output_table_entries, + A2B_or_B2A* out) { + // byte_width is 1 or 2, [input|output]_table_entries are in [2, 4096], so no overflow + uint32_t byte_len_per_input_table = input_table_entries * byte_width; + uint32_t byte_len_per_output_table = output_table_entries * byte_width; + + // [input|output]_channels are <= 4, so still no overflow + uint32_t byte_len_all_input_tables = out->input_channels * byte_len_per_input_table; + uint32_t byte_len_all_output_tables = out->output_channels * byte_len_per_output_table; + + uint64_t grid_size = out->output_channels * byte_width; + for (uint32_t axis = 0; axis < out->input_channels; ++axis) { + grid_size *= out->grid_points[axis]; + } + + if (max_tables_len < byte_len_all_input_tables + grid_size + byte_len_all_output_tables) { + return false; + } + + for (uint32_t i = 0; i < out->input_channels; ++i) { + out->input_curves[i].table_entries = input_table_entries; + if (byte_width == 1) { + out->input_curves[i].table_8 = table_base + i * byte_len_per_input_table; + out->input_curves[i].table_16 = nullptr; + } else { + out->input_curves[i].table_8 = nullptr; + out->input_curves[i].table_16 = table_base + i * byte_len_per_input_table; + } + } + + if (byte_width == 1) { + out->grid_8 = table_base + byte_len_all_input_tables; + out->grid_16 = nullptr; + } else { + out->grid_8 = nullptr; + out->grid_16 = table_base + byte_len_all_input_tables; + } + + const uint8_t* output_table_base = table_base + byte_len_all_input_tables + grid_size; + for (uint32_t i = 0; i < out->output_channels; ++i) { + out->output_curves[i].table_entries = output_table_entries; + if (byte_width == 1) { + out->output_curves[i].table_8 = output_table_base + i * byte_len_per_output_table; + out->output_curves[i].table_16 = nullptr; + } else { + out->output_curves[i].table_8 = nullptr; + out->output_curves[i].table_16 = output_table_base + i * byte_len_per_output_table; + } + } + + return true; +} + +template +static bool read_tag_mft1(const skcms_ICCTag* tag, A2B_or_B2A* out) { + if (tag->size < SAFE_FIXED_SIZE(mft1_Layout)) { + return false; + } + + const mft1_Layout* mftTag = (const mft1_Layout*)tag->buf; + if (!read_mft_common(mftTag->common, out)) { + return false; + } + + uint32_t input_table_entries = 256; + uint32_t output_table_entries = 256; + + return init_tables(mftTag->variable, tag->size - SAFE_FIXED_SIZE(mft1_Layout), 1, + input_table_entries, output_table_entries, out); +} + +template +static bool read_tag_mft2(const skcms_ICCTag* tag, A2B_or_B2A* out) { + if (tag->size < SAFE_FIXED_SIZE(mft2_Layout)) { + return false; + } + + const mft2_Layout* mftTag = (const mft2_Layout*)tag->buf; + if (!read_mft_common(mftTag->common, out)) { + return false; + } + + uint32_t input_table_entries = read_big_u16(mftTag->input_table_entries); + uint32_t output_table_entries = read_big_u16(mftTag->output_table_entries); + + // ICC spec mandates that 2 <= table_entries <= 4096 + if (input_table_entries < 2 || input_table_entries > 4096 || + output_table_entries < 2 || output_table_entries > 4096) { + return false; + } + + return init_tables(mftTag->variable, tag->size - SAFE_FIXED_SIZE(mft2_Layout), 2, + input_table_entries, output_table_entries, out); +} + +static bool read_curves(const uint8_t* buf, uint32_t size, uint32_t curve_offset, + uint32_t num_curves, skcms_Curve* curves) { + for (uint32_t i = 0; i < num_curves; ++i) { + if (curve_offset > size) { + return false; + } + + uint32_t curve_bytes; + if (!read_curve(buf + curve_offset, size - curve_offset, &curves[i], &curve_bytes)) { + return false; + } + + if (curve_bytes > UINT32_MAX - 3) { + return false; + } + curve_bytes = (curve_bytes + 3) & ~3U; + + uint64_t new_offset_64 = (uint64_t)curve_offset + curve_bytes; + curve_offset = (uint32_t)new_offset_64; + if (new_offset_64 != curve_offset) { + return false; + } + } + + return true; +} + +// mAB and mBA tags use the same encoding, including color lookup tables. +typedef struct { + uint8_t type [ 4]; + uint8_t reserved_a [ 4]; + uint8_t input_channels [ 1]; + uint8_t output_channels [ 1]; + uint8_t reserved_b [ 2]; + uint8_t b_curve_offset [ 4]; + uint8_t matrix_offset [ 4]; + uint8_t m_curve_offset [ 4]; + uint8_t clut_offset [ 4]; + uint8_t a_curve_offset [ 4]; +} mAB_or_mBA_Layout; + +typedef struct { + uint8_t grid_points [16]; + uint8_t grid_byte_width [ 1]; + uint8_t reserved [ 3]; + uint8_t variable [1/*variable*/]; +} CLUT_Layout; + +static bool read_tag_mab(const skcms_ICCTag* tag, skcms_A2B* a2b, bool pcs_is_xyz) { + if (tag->size < SAFE_SIZEOF(mAB_or_mBA_Layout)) { + return false; + } + + const mAB_or_mBA_Layout* mABTag = (const mAB_or_mBA_Layout*)tag->buf; + + a2b->input_channels = mABTag->input_channels[0]; + a2b->output_channels = mABTag->output_channels[0]; + + // We require exactly three (ie XYZ/Lab/RGB) output channels + if (a2b->output_channels != ARRAY_COUNT(a2b->output_curves)) { + return false; + } + // We require no more than four (ie CMYK) input channels + if (a2b->input_channels > ARRAY_COUNT(a2b->input_curves)) { + return false; + } + + uint32_t b_curve_offset = read_big_u32(mABTag->b_curve_offset); + uint32_t matrix_offset = read_big_u32(mABTag->matrix_offset); + uint32_t m_curve_offset = read_big_u32(mABTag->m_curve_offset); + uint32_t clut_offset = read_big_u32(mABTag->clut_offset); + uint32_t a_curve_offset = read_big_u32(mABTag->a_curve_offset); + + // "B" curves must be present + if (0 == b_curve_offset) { + return false; + } + + if (!read_curves(tag->buf, tag->size, b_curve_offset, a2b->output_channels, + a2b->output_curves)) { + return false; + } + + // "M" curves and Matrix must be used together + if (0 != m_curve_offset) { + if (0 == matrix_offset) { + return false; + } + a2b->matrix_channels = a2b->output_channels; + if (!read_curves(tag->buf, tag->size, m_curve_offset, a2b->matrix_channels, + a2b->matrix_curves)) { + return false; + } + + // Read matrix, which is stored as a row-major 3x3, followed by the fourth column + if (tag->size < matrix_offset + 12 * SAFE_SIZEOF(uint32_t)) { + return false; + } + float encoding_factor = pcs_is_xyz ? (65535 / 32768.0f) : 1.0f; + const uint8_t* mtx_buf = tag->buf + matrix_offset; + a2b->matrix.vals[0][0] = encoding_factor * read_big_fixed(mtx_buf + 0); + a2b->matrix.vals[0][1] = encoding_factor * read_big_fixed(mtx_buf + 4); + a2b->matrix.vals[0][2] = encoding_factor * read_big_fixed(mtx_buf + 8); + a2b->matrix.vals[1][0] = encoding_factor * read_big_fixed(mtx_buf + 12); + a2b->matrix.vals[1][1] = encoding_factor * read_big_fixed(mtx_buf + 16); + a2b->matrix.vals[1][2] = encoding_factor * read_big_fixed(mtx_buf + 20); + a2b->matrix.vals[2][0] = encoding_factor * read_big_fixed(mtx_buf + 24); + a2b->matrix.vals[2][1] = encoding_factor * read_big_fixed(mtx_buf + 28); + a2b->matrix.vals[2][2] = encoding_factor * read_big_fixed(mtx_buf + 32); + a2b->matrix.vals[0][3] = encoding_factor * read_big_fixed(mtx_buf + 36); + a2b->matrix.vals[1][3] = encoding_factor * read_big_fixed(mtx_buf + 40); + a2b->matrix.vals[2][3] = encoding_factor * read_big_fixed(mtx_buf + 44); + } else { + if (0 != matrix_offset) { + return false; + } + a2b->matrix_channels = 0; + } + + // "A" curves and CLUT must be used together + if (0 != a_curve_offset) { + if (0 == clut_offset) { + return false; + } + if (!read_curves(tag->buf, tag->size, a_curve_offset, a2b->input_channels, + a2b->input_curves)) { + return false; + } + + if (tag->size < clut_offset + SAFE_FIXED_SIZE(CLUT_Layout)) { + return false; + } + const CLUT_Layout* clut = (const CLUT_Layout*)(tag->buf + clut_offset); + + if (clut->grid_byte_width[0] == 1) { + a2b->grid_8 = clut->variable; + a2b->grid_16 = nullptr; + } else if (clut->grid_byte_width[0] == 2) { + a2b->grid_8 = nullptr; + a2b->grid_16 = clut->variable; + } else { + return false; + } + + uint64_t grid_size = a2b->output_channels * clut->grid_byte_width[0]; // the payload + for (uint32_t i = 0; i < a2b->input_channels; ++i) { + a2b->grid_points[i] = clut->grid_points[i]; + // The grid only makes sense with at least two points along each axis + if (a2b->grid_points[i] < 2) { + return false; + } + grid_size *= a2b->grid_points[i]; + } + if (tag->size < clut_offset + SAFE_FIXED_SIZE(CLUT_Layout) + grid_size) { + return false; + } + } else { + if (0 != clut_offset) { + return false; + } + + // If there is no CLUT, the number of input and output channels must match + if (a2b->input_channels != a2b->output_channels) { + return false; + } + + // Zero out the number of input channels to signal that we're skipping this stage + a2b->input_channels = 0; + } + + return true; +} + +// Exactly the same as read_tag_mab(), except where there are comments. +// TODO: refactor the two to eliminate common code? +static bool read_tag_mba(const skcms_ICCTag* tag, skcms_B2A* b2a, bool pcs_is_xyz) { + if (tag->size < SAFE_SIZEOF(mAB_or_mBA_Layout)) { + return false; + } + + const mAB_or_mBA_Layout* mBATag = (const mAB_or_mBA_Layout*)tag->buf; + + b2a->input_channels = mBATag->input_channels[0]; + b2a->output_channels = mBATag->output_channels[0]; + + // Require exactly 3 inputs (XYZ) and 3 (RGB) or 4 (CMYK) outputs. + if (b2a->input_channels != ARRAY_COUNT(b2a->input_curves)) { + return false; + } + if (b2a->output_channels < 3 || b2a->output_channels > ARRAY_COUNT(b2a->output_curves)) { + return false; + } + + uint32_t b_curve_offset = read_big_u32(mBATag->b_curve_offset); + uint32_t matrix_offset = read_big_u32(mBATag->matrix_offset); + uint32_t m_curve_offset = read_big_u32(mBATag->m_curve_offset); + uint32_t clut_offset = read_big_u32(mBATag->clut_offset); + uint32_t a_curve_offset = read_big_u32(mBATag->a_curve_offset); + + if (0 == b_curve_offset) { + return false; + } + + // "B" curves are our inputs, not outputs. + if (!read_curves(tag->buf, tag->size, b_curve_offset, b2a->input_channels, + b2a->input_curves)) { + return false; + } + + if (0 != m_curve_offset) { + if (0 == matrix_offset) { + return false; + } + // Matrix channels is tied to input_channels (3), not output_channels. + b2a->matrix_channels = b2a->input_channels; + + if (!read_curves(tag->buf, tag->size, m_curve_offset, b2a->matrix_channels, + b2a->matrix_curves)) { + return false; + } + + if (tag->size < matrix_offset + 12 * SAFE_SIZEOF(uint32_t)) { + return false; + } + float encoding_factor = pcs_is_xyz ? (32768 / 65535.0f) : 1.0f; // TODO: understand + const uint8_t* mtx_buf = tag->buf + matrix_offset; + b2a->matrix.vals[0][0] = encoding_factor * read_big_fixed(mtx_buf + 0); + b2a->matrix.vals[0][1] = encoding_factor * read_big_fixed(mtx_buf + 4); + b2a->matrix.vals[0][2] = encoding_factor * read_big_fixed(mtx_buf + 8); + b2a->matrix.vals[1][0] = encoding_factor * read_big_fixed(mtx_buf + 12); + b2a->matrix.vals[1][1] = encoding_factor * read_big_fixed(mtx_buf + 16); + b2a->matrix.vals[1][2] = encoding_factor * read_big_fixed(mtx_buf + 20); + b2a->matrix.vals[2][0] = encoding_factor * read_big_fixed(mtx_buf + 24); + b2a->matrix.vals[2][1] = encoding_factor * read_big_fixed(mtx_buf + 28); + b2a->matrix.vals[2][2] = encoding_factor * read_big_fixed(mtx_buf + 32); + b2a->matrix.vals[0][3] = encoding_factor * read_big_fixed(mtx_buf + 36); + b2a->matrix.vals[1][3] = encoding_factor * read_big_fixed(mtx_buf + 40); + b2a->matrix.vals[2][3] = encoding_factor * read_big_fixed(mtx_buf + 44); + } else { + if (0 != matrix_offset) { + return false; + } + b2a->matrix_channels = 0; + } + + if (0 != a_curve_offset) { + if (0 == clut_offset) { + return false; + } + + // "A" curves are our output, not input. + if (!read_curves(tag->buf, tag->size, a_curve_offset, b2a->output_channels, + b2a->output_curves)) { + return false; + } + + if (tag->size < clut_offset + SAFE_FIXED_SIZE(CLUT_Layout)) { + return false; + } + const CLUT_Layout* clut = (const CLUT_Layout*)(tag->buf + clut_offset); + + if (clut->grid_byte_width[0] == 1) { + b2a->grid_8 = clut->variable; + b2a->grid_16 = nullptr; + } else if (clut->grid_byte_width[0] == 2) { + b2a->grid_8 = nullptr; + b2a->grid_16 = clut->variable; + } else { + return false; + } + + uint64_t grid_size = b2a->output_channels * clut->grid_byte_width[0]; + for (uint32_t i = 0; i < b2a->input_channels; ++i) { + b2a->grid_points[i] = clut->grid_points[i]; + if (b2a->grid_points[i] < 2) { + return false; + } + grid_size *= b2a->grid_points[i]; + } + if (tag->size < clut_offset + SAFE_FIXED_SIZE(CLUT_Layout) + grid_size) { + return false; + } + } else { + if (0 != clut_offset) { + return false; + } + + if (b2a->input_channels != b2a->output_channels) { + return false; + } + + // Zero out *output* channels to skip this stage. + b2a->output_channels = 0; + } + return true; +} + +// If you pass f, we'll fit a possibly-non-zero value for *f. +// If you pass nullptr, we'll assume you want *f to be treated as zero. +static int fit_linear(const skcms_Curve* curve, int N, float tol, + float* c, float* d, float* f = nullptr) { + assert(N > 1); + // We iteratively fit the first points to the TF's linear piece. + // We want the cx + f line to pass through the first and last points we fit exactly. + // + // As we walk along the points we find the minimum and maximum slope of the line before the + // error would exceed our tolerance. We stop when the range [slope_min, slope_max] becomes + // emtpy, when we definitely can't add any more points. + // + // Some points' error intervals may intersect the running interval but not lie fully + // within it. So we keep track of the last point we saw that is a valid end point candidate, + // and once the search is done, back up to build the line through *that* point. + const float dx = 1.0f / static_cast(N - 1); + + int lin_points = 1; + + float f_zero = 0.0f; + if (f) { + *f = eval_curve(curve, 0); + } else { + f = &f_zero; + } + + + float slope_min = -INFINITY_; + float slope_max = +INFINITY_; + for (int i = 1; i < N; ++i) { + float x = static_cast(i) * dx; + float y = eval_curve(curve, x); + + float slope_max_i = (y + tol - *f) / x, + slope_min_i = (y - tol - *f) / x; + if (slope_max_i < slope_min || slope_max < slope_min_i) { + // Slope intervals would no longer overlap. + break; + } + slope_max = fminf_(slope_max, slope_max_i); + slope_min = fmaxf_(slope_min, slope_min_i); + + float cur_slope = (y - *f) / x; + if (slope_min <= cur_slope && cur_slope <= slope_max) { + lin_points = i + 1; + *c = cur_slope; + } + } + + // Set D to the last point that met our tolerance. + *d = static_cast(lin_points - 1) * dx; + return lin_points; +} + +// If this skcms_Curve holds an identity table, rewrite it as an identity skcms_TransferFunction. +static void canonicalize_identity(skcms_Curve* curve) { + if (curve->table_entries && curve->table_entries <= (uint32_t)INT_MAX) { + int N = (int)curve->table_entries; + + float c = 0.0f, d = 0.0f, f = 0.0f; + if (N == fit_linear(curve, N, 1.0f/static_cast(2*N), &c,&d,&f) + && c == 1.0f + && f == 0.0f) { + curve->table_entries = 0; + curve->table_8 = nullptr; + curve->table_16 = nullptr; + curve->parametric = skcms_TransferFunction{1,1,0,0,0,0,0}; + } + } +} + +static bool read_a2b(const skcms_ICCTag* tag, skcms_A2B* a2b, bool pcs_is_xyz) { + bool ok = false; + if (tag->type == skcms_Signature_mft1) { ok = read_tag_mft1(tag, a2b); } + if (tag->type == skcms_Signature_mft2) { ok = read_tag_mft2(tag, a2b); } + if (tag->type == skcms_Signature_mAB ) { ok = read_tag_mab(tag, a2b, pcs_is_xyz); } + if (!ok) { + return false; + } + + if (a2b->input_channels > 0) { canonicalize_identity(a2b->input_curves + 0); } + if (a2b->input_channels > 1) { canonicalize_identity(a2b->input_curves + 1); } + if (a2b->input_channels > 2) { canonicalize_identity(a2b->input_curves + 2); } + if (a2b->input_channels > 3) { canonicalize_identity(a2b->input_curves + 3); } + + if (a2b->matrix_channels > 0) { canonicalize_identity(a2b->matrix_curves + 0); } + if (a2b->matrix_channels > 1) { canonicalize_identity(a2b->matrix_curves + 1); } + if (a2b->matrix_channels > 2) { canonicalize_identity(a2b->matrix_curves + 2); } + + if (a2b->output_channels > 0) { canonicalize_identity(a2b->output_curves + 0); } + if (a2b->output_channels > 1) { canonicalize_identity(a2b->output_curves + 1); } + if (a2b->output_channels > 2) { canonicalize_identity(a2b->output_curves + 2); } + + return true; +} + +static bool read_b2a(const skcms_ICCTag* tag, skcms_B2A* b2a, bool pcs_is_xyz) { + bool ok = false; + if (tag->type == skcms_Signature_mft1) { ok = read_tag_mft1(tag, b2a); } + if (tag->type == skcms_Signature_mft2) { ok = read_tag_mft2(tag, b2a); } + if (tag->type == skcms_Signature_mBA ) { ok = read_tag_mba(tag, b2a, pcs_is_xyz); } + if (!ok) { + return false; + } + + if (b2a->input_channels > 0) { canonicalize_identity(b2a->input_curves + 0); } + if (b2a->input_channels > 1) { canonicalize_identity(b2a->input_curves + 1); } + if (b2a->input_channels > 2) { canonicalize_identity(b2a->input_curves + 2); } + + if (b2a->matrix_channels > 0) { canonicalize_identity(b2a->matrix_curves + 0); } + if (b2a->matrix_channels > 1) { canonicalize_identity(b2a->matrix_curves + 1); } + if (b2a->matrix_channels > 2) { canonicalize_identity(b2a->matrix_curves + 2); } + + if (b2a->output_channels > 0) { canonicalize_identity(b2a->output_curves + 0); } + if (b2a->output_channels > 1) { canonicalize_identity(b2a->output_curves + 1); } + if (b2a->output_channels > 2) { canonicalize_identity(b2a->output_curves + 2); } + if (b2a->output_channels > 3) { canonicalize_identity(b2a->output_curves + 3); } + + return true; +} + +typedef struct { + uint8_t type [4]; + uint8_t reserved [4]; + uint8_t color_primaries [1]; + uint8_t transfer_characteristics [1]; + uint8_t matrix_coefficients [1]; + uint8_t video_full_range_flag [1]; +} CICP_Layout; + +static bool read_cicp(const skcms_ICCTag* tag, skcms_CICP* cicp) { + if (tag->type != skcms_Signature_CICP || tag->size < SAFE_SIZEOF(CICP_Layout)) { + return false; + } + + const CICP_Layout* cicpTag = (const CICP_Layout*)tag->buf; + + cicp->color_primaries = cicpTag->color_primaries[0]; + cicp->transfer_characteristics = cicpTag->transfer_characteristics[0]; + cicp->matrix_coefficients = cicpTag->matrix_coefficients[0]; + cicp->video_full_range_flag = cicpTag->video_full_range_flag[0]; + return true; +} + +void skcms_GetTagByIndex(const skcms_ICCProfile* profile, uint32_t idx, skcms_ICCTag* tag) { + if (!profile || !profile->buffer || !tag) { return; } + if (idx > profile->tag_count) { return; } + const tag_Layout* tags = get_tag_table(profile); + tag->signature = read_big_u32(tags[idx].signature); + tag->size = read_big_u32(tags[idx].size); + tag->buf = read_big_u32(tags[idx].offset) + profile->buffer; + tag->type = read_big_u32(tag->buf); +} + +bool skcms_GetTagBySignature(const skcms_ICCProfile* profile, uint32_t sig, skcms_ICCTag* tag) { + if (!profile || !profile->buffer || !tag) { return false; } + const tag_Layout* tags = get_tag_table(profile); + for (uint32_t i = 0; i < profile->tag_count; ++i) { + if (read_big_u32(tags[i].signature) == sig) { + tag->signature = sig; + tag->size = read_big_u32(tags[i].size); + tag->buf = read_big_u32(tags[i].offset) + profile->buffer; + tag->type = read_big_u32(tag->buf); + return true; + } + } + return false; +} + +static bool usable_as_src(const skcms_ICCProfile* profile) { + return profile->has_A2B + || (profile->has_trc && profile->has_toXYZD50); +} + +bool skcms_ParseWithA2BPriority(const void* buf, size_t len, + const int priority[], const int priorities, + skcms_ICCProfile* profile) { + static_assert(SAFE_SIZEOF(header_Layout) == 132, "need to update header code"); + + if (!profile) { + return false; + } + memset(profile, 0, SAFE_SIZEOF(*profile)); + + if (len < SAFE_SIZEOF(header_Layout)) { + return false; + } + + // Byte-swap all header fields + const header_Layout* header = (const header_Layout*)buf; + profile->buffer = (const uint8_t*)buf; + profile->size = read_big_u32(header->size); + uint32_t version = read_big_u32(header->version); + profile->data_color_space = read_big_u32(header->data_color_space); + profile->pcs = read_big_u32(header->pcs); + uint32_t signature = read_big_u32(header->signature); + float illuminant_X = read_big_fixed(header->illuminant_X); + float illuminant_Y = read_big_fixed(header->illuminant_Y); + float illuminant_Z = read_big_fixed(header->illuminant_Z); + profile->tag_count = read_big_u32(header->tag_count); + + // Validate signature, size (smaller than buffer, large enough to hold tag table), + // and major version + uint64_t tag_table_size = profile->tag_count * SAFE_SIZEOF(tag_Layout); + if (signature != skcms_Signature_acsp || + profile->size > len || + profile->size < SAFE_SIZEOF(header_Layout) + tag_table_size || + (version >> 24) > 4) { + return false; + } + + // Validate that illuminant is D50 white + if (fabsf_(illuminant_X - 0.9642f) > 0.0100f || + fabsf_(illuminant_Y - 1.0000f) > 0.0100f || + fabsf_(illuminant_Z - 0.8249f) > 0.0100f) { + return false; + } + + // Validate that all tag entries have sane offset + size + const tag_Layout* tags = get_tag_table(profile); + for (uint32_t i = 0; i < profile->tag_count; ++i) { + uint32_t tag_offset = read_big_u32(tags[i].offset); + uint32_t tag_size = read_big_u32(tags[i].size); + uint64_t tag_end = (uint64_t)tag_offset + (uint64_t)tag_size; + if (tag_size < 4 || tag_end > profile->size) { + return false; + } + } + + if (profile->pcs != skcms_Signature_XYZ && profile->pcs != skcms_Signature_Lab) { + return false; + } + + bool pcs_is_xyz = profile->pcs == skcms_Signature_XYZ; + + // Pre-parse commonly used tags. + skcms_ICCTag kTRC; + if (profile->data_color_space == skcms_Signature_Gray && + skcms_GetTagBySignature(profile, skcms_Signature_kTRC, &kTRC)) { + if (!read_curve(kTRC.buf, kTRC.size, &profile->trc[0], nullptr)) { + // Malformed tag + return false; + } + profile->trc[1] = profile->trc[0]; + profile->trc[2] = profile->trc[0]; + profile->has_trc = true; + + if (pcs_is_xyz) { + profile->toXYZD50.vals[0][0] = illuminant_X; + profile->toXYZD50.vals[1][1] = illuminant_Y; + profile->toXYZD50.vals[2][2] = illuminant_Z; + profile->has_toXYZD50 = true; + } + } else { + skcms_ICCTag rTRC, gTRC, bTRC; + if (skcms_GetTagBySignature(profile, skcms_Signature_rTRC, &rTRC) && + skcms_GetTagBySignature(profile, skcms_Signature_gTRC, &gTRC) && + skcms_GetTagBySignature(profile, skcms_Signature_bTRC, &bTRC)) { + if (!read_curve(rTRC.buf, rTRC.size, &profile->trc[0], nullptr) || + !read_curve(gTRC.buf, gTRC.size, &profile->trc[1], nullptr) || + !read_curve(bTRC.buf, bTRC.size, &profile->trc[2], nullptr)) { + // Malformed TRC tags + return false; + } + profile->has_trc = true; + } + + skcms_ICCTag rXYZ, gXYZ, bXYZ; + if (skcms_GetTagBySignature(profile, skcms_Signature_rXYZ, &rXYZ) && + skcms_GetTagBySignature(profile, skcms_Signature_gXYZ, &gXYZ) && + skcms_GetTagBySignature(profile, skcms_Signature_bXYZ, &bXYZ)) { + if (!read_to_XYZD50(&rXYZ, &gXYZ, &bXYZ, &profile->toXYZD50)) { + // Malformed XYZ tags + return false; + } + profile->has_toXYZD50 = true; + } + } + + for (int i = 0; i < priorities; i++) { + // enum { perceptual, relative_colormetric, saturation } + if (priority[i] < 0 || priority[i] > 2) { + return false; + } + uint32_t sig = skcms_Signature_A2B0 + static_cast(priority[i]); + skcms_ICCTag tag; + if (skcms_GetTagBySignature(profile, sig, &tag)) { + if (!read_a2b(&tag, &profile->A2B, pcs_is_xyz)) { + // Malformed A2B tag + return false; + } + profile->has_A2B = true; + break; + } + } + + for (int i = 0; i < priorities; i++) { + // enum { perceptual, relative_colormetric, saturation } + if (priority[i] < 0 || priority[i] > 2) { + return false; + } + uint32_t sig = skcms_Signature_B2A0 + static_cast(priority[i]); + skcms_ICCTag tag; + if (skcms_GetTagBySignature(profile, sig, &tag)) { + if (!read_b2a(&tag, &profile->B2A, pcs_is_xyz)) { + // Malformed B2A tag + return false; + } + profile->has_B2A = true; + break; + } + } + + skcms_ICCTag cicp_tag; + if (skcms_GetTagBySignature(profile, skcms_Signature_CICP, &cicp_tag)) { + if (!read_cicp(&cicp_tag, &profile->CICP)) { + // Malformed CICP tag + return false; + } + profile->has_CICP = true; + } + + return usable_as_src(profile); +} + + +const skcms_ICCProfile* skcms_sRGB_profile() { + static const skcms_ICCProfile sRGB_profile = { + nullptr, // buffer, moot here + + 0, // size, moot here + skcms_Signature_RGB, // data_color_space + skcms_Signature_XYZ, // pcs + 0, // tag count, moot here + + // We choose to represent sRGB with its canonical transfer function, + // and with its canonical XYZD50 gamut matrix. + true, // has_trc, followed by the 3 trc curves + { + {{0, {2.4f, (float)(1/1.055), (float)(0.055/1.055), (float)(1/12.92), 0.04045f, 0, 0}}}, + {{0, {2.4f, (float)(1/1.055), (float)(0.055/1.055), (float)(1/12.92), 0.04045f, 0, 0}}}, + {{0, {2.4f, (float)(1/1.055), (float)(0.055/1.055), (float)(1/12.92), 0.04045f, 0, 0}}}, + }, + + true, // has_toXYZD50, followed by 3x3 toXYZD50 matrix + {{ + { 0.436065674f, 0.385147095f, 0.143066406f }, + { 0.222488403f, 0.716873169f, 0.060607910f }, + { 0.013916016f, 0.097076416f, 0.714096069f }, + }}, + + false, // has_A2B, followed by A2B itself, which we don't care about. + { + 0, + { + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + }, + {0,0,0,0}, + nullptr, + nullptr, + + 0, + { + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + }, + {{ + { 0,0,0,0 }, + { 0,0,0,0 }, + { 0,0,0,0 }, + }}, + + 0, + { + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + }, + }, + + false, // has_B2A, followed by B2A itself, which we also don't care about. + { + 0, + { + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + }, + + 0, + {{ + { 0,0,0,0 }, + { 0,0,0,0 }, + { 0,0,0,0 }, + }}, + { + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + }, + + 0, + {0,0,0,0}, + nullptr, + nullptr, + { + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + }, + }, + + false, // has_CICP, followed by cicp itself which we don't care about. + { 0, 0, 0, 0 }, + }; + return &sRGB_profile; +} + +const skcms_ICCProfile* skcms_XYZD50_profile() { + // Just like sRGB above, but with identity transfer functions and toXYZD50 matrix. + static const skcms_ICCProfile XYZD50_profile = { + nullptr, // buffer, moot here + + 0, // size, moot here + skcms_Signature_RGB, // data_color_space + skcms_Signature_XYZ, // pcs + 0, // tag count, moot here + + true, // has_trc, followed by the 3 trc curves + { + {{0, {1,1, 0,0,0,0,0}}}, + {{0, {1,1, 0,0,0,0,0}}}, + {{0, {1,1, 0,0,0,0,0}}}, + }, + + true, // has_toXYZD50, followed by 3x3 toXYZD50 matrix + {{ + { 1,0,0 }, + { 0,1,0 }, + { 0,0,1 }, + }}, + + false, // has_A2B, followed by A2B itself, which we don't care about. + { + 0, + { + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + }, + {0,0,0,0}, + nullptr, + nullptr, + + 0, + { + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + }, + {{ + { 0,0,0,0 }, + { 0,0,0,0 }, + { 0,0,0,0 }, + }}, + + 0, + { + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + }, + }, + + false, // has_B2A, followed by B2A itself, which we also don't care about. + { + 0, + { + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + }, + + 0, + {{ + { 0,0,0,0 }, + { 0,0,0,0 }, + { 0,0,0,0 }, + }}, + { + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + }, + + 0, + {0,0,0,0}, + nullptr, + nullptr, + { + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + {{0, {0,0, 0,0,0,0,0}}}, + }, + }, + + false, // has_CICP, followed by cicp itself which we don't care about. + { 0, 0, 0, 0 }, + }; + + return &XYZD50_profile; +} + +const skcms_TransferFunction* skcms_sRGB_TransferFunction() { + return &skcms_sRGB_profile()->trc[0].parametric; +} + +const skcms_TransferFunction* skcms_sRGB_Inverse_TransferFunction() { + static const skcms_TransferFunction sRGB_inv = + {0.416666657f, 1.137283325f, -0.0f, 12.920000076f, 0.003130805f, -0.054969788f, -0.0f}; + return &sRGB_inv; +} + +const skcms_TransferFunction* skcms_Identity_TransferFunction() { + static const skcms_TransferFunction identity = {1,1,0,0,0,0,0}; + return &identity; +} + +const uint8_t skcms_252_random_bytes[] = { + 8, 179, 128, 204, 253, 38, 134, 184, 68, 102, 32, 138, 99, 39, 169, 215, + 119, 26, 3, 223, 95, 239, 52, 132, 114, 74, 81, 234, 97, 116, 244, 205, 30, + 154, 173, 12, 51, 159, 122, 153, 61, 226, 236, 178, 229, 55, 181, 220, 191, + 194, 160, 126, 168, 82, 131, 18, 180, 245, 163, 22, 246, 69, 235, 252, 57, + 108, 14, 6, 152, 240, 255, 171, 242, 20, 227, 177, 238, 96, 85, 16, 211, + 70, 200, 149, 155, 146, 127, 145, 100, 151, 109, 19, 165, 208, 195, 164, + 137, 254, 182, 248, 64, 201, 45, 209, 5, 147, 207, 210, 113, 162, 83, 225, + 9, 31, 15, 231, 115, 37, 58, 53, 24, 49, 197, 56, 120, 172, 48, 21, 214, + 129, 111, 11, 50, 187, 196, 34, 60, 103, 71, 144, 47, 203, 77, 80, 232, + 140, 222, 250, 206, 166, 247, 139, 249, 221, 72, 106, 27, 199, 117, 54, + 219, 135, 118, 40, 79, 41, 251, 46, 93, 212, 92, 233, 148, 28, 121, 63, + 123, 158, 105, 59, 29, 42, 143, 23, 0, 107, 176, 87, 104, 183, 156, 193, + 189, 90, 188, 65, 190, 17, 198, 7, 186, 161, 1, 124, 78, 125, 170, 133, + 174, 218, 67, 157, 75, 101, 89, 217, 62, 33, 141, 228, 25, 35, 91, 230, 4, + 2, 13, 73, 86, 167, 237, 84, 243, 44, 185, 66, 130, 110, 150, 142, 216, 88, + 112, 36, 224, 136, 202, 76, 94, 98, 175, 213 +}; + +bool skcms_ApproximatelyEqualProfiles(const skcms_ICCProfile* A, const skcms_ICCProfile* B) { + // Test for exactly equal profiles first. + if (A == B || 0 == memcmp(A,B, sizeof(skcms_ICCProfile))) { + return true; + } + + // For now this is the essentially the same strategy we use in test_only.c + // for our skcms_Transform() smoke tests: + // 1) transform A to XYZD50 + // 2) transform B to XYZD50 + // 3) return true if they're similar enough + // Our current criterion in 3) is maximum 1 bit error per XYZD50 byte. + + // skcms_252_random_bytes are 252 of a random shuffle of all possible bytes. + // 252 is evenly divisible by 3 and 4. Only 192, 10, 241, and 43 are missing. + + // We want to allow otherwise equivalent profiles tagged as grayscale and RGB + // to be treated as equal. But CMYK profiles are a totally different ballgame. + const auto CMYK = skcms_Signature_CMYK; + if ((A->data_color_space == CMYK) != (B->data_color_space == CMYK)) { + return false; + } + + // Interpret as RGB_888 if data color space is RGB or GRAY, RGBA_8888 if CMYK. + // TODO: working with RGBA_8888 either way is probably fastest. + skcms_PixelFormat fmt = skcms_PixelFormat_RGB_888; + size_t npixels = 84; + if (A->data_color_space == skcms_Signature_CMYK) { + fmt = skcms_PixelFormat_RGBA_8888; + npixels = 63; + } + + // TODO: if A or B is a known profile (skcms_sRGB_profile, skcms_XYZD50_profile), + // use pre-canned results and skip that skcms_Transform() call? + uint8_t dstA[252], + dstB[252]; + if (!skcms_Transform( + skcms_252_random_bytes, fmt, skcms_AlphaFormat_Unpremul, A, + dstA, skcms_PixelFormat_RGB_888, skcms_AlphaFormat_Unpremul, skcms_XYZD50_profile(), + npixels)) { + return false; + } + if (!skcms_Transform( + skcms_252_random_bytes, fmt, skcms_AlphaFormat_Unpremul, B, + dstB, skcms_PixelFormat_RGB_888, skcms_AlphaFormat_Unpremul, skcms_XYZD50_profile(), + npixels)) { + return false; + } + + // TODO: make sure this final check has reasonable codegen. + for (size_t i = 0; i < 252; i++) { + if (abs((int)dstA[i] - (int)dstB[i]) > 1) { + return false; + } + } + return true; +} + +bool skcms_TRCs_AreApproximateInverse(const skcms_ICCProfile* profile, + const skcms_TransferFunction* inv_tf) { + if (!profile || !profile->has_trc) { + return false; + } + + return skcms_AreApproximateInverses(&profile->trc[0], inv_tf) && + skcms_AreApproximateInverses(&profile->trc[1], inv_tf) && + skcms_AreApproximateInverses(&profile->trc[2], inv_tf); +} + +static bool is_zero_to_one(float x) { + return 0 <= x && x <= 1; +} + +typedef struct { float vals[3]; } skcms_Vector3; + +static skcms_Vector3 mv_mul(const skcms_Matrix3x3* m, const skcms_Vector3* v) { + skcms_Vector3 dst = {{0,0,0}}; + for (int row = 0; row < 3; ++row) { + dst.vals[row] = m->vals[row][0] * v->vals[0] + + m->vals[row][1] * v->vals[1] + + m->vals[row][2] * v->vals[2]; + } + return dst; +} + +bool skcms_AdaptToXYZD50(float wx, float wy, + skcms_Matrix3x3* toXYZD50) { + if (!is_zero_to_one(wx) || !is_zero_to_one(wy) || + !toXYZD50) { + return false; + } + + // Assumes that Y is 1.0f. + skcms_Vector3 wXYZ = { { wx / wy, 1, (1 - wx - wy) / wy } }; + + // Now convert toXYZ matrix to toXYZD50. + skcms_Vector3 wXYZD50 = { { 0.96422f, 1.0f, 0.82521f } }; + + // Calculate the chromatic adaptation matrix. We will use the Bradford method, thus + // the matrices below. The Bradford method is used by Adobe and is widely considered + // to be the best. + skcms_Matrix3x3 xyz_to_lms = {{ + { 0.8951f, 0.2664f, -0.1614f }, + { -0.7502f, 1.7135f, 0.0367f }, + { 0.0389f, -0.0685f, 1.0296f }, + }}; + skcms_Matrix3x3 lms_to_xyz = {{ + { 0.9869929f, -0.1470543f, 0.1599627f }, + { 0.4323053f, 0.5183603f, 0.0492912f }, + { -0.0085287f, 0.0400428f, 0.9684867f }, + }}; + + skcms_Vector3 srcCone = mv_mul(&xyz_to_lms, &wXYZ); + skcms_Vector3 dstCone = mv_mul(&xyz_to_lms, &wXYZD50); + + *toXYZD50 = {{ + { dstCone.vals[0] / srcCone.vals[0], 0, 0 }, + { 0, dstCone.vals[1] / srcCone.vals[1], 0 }, + { 0, 0, dstCone.vals[2] / srcCone.vals[2] }, + }}; + *toXYZD50 = skcms_Matrix3x3_concat(toXYZD50, &xyz_to_lms); + *toXYZD50 = skcms_Matrix3x3_concat(&lms_to_xyz, toXYZD50); + + return true; +} + +bool skcms_PrimariesToXYZD50(float rx, float ry, + float gx, float gy, + float bx, float by, + float wx, float wy, + skcms_Matrix3x3* toXYZD50) { + if (!is_zero_to_one(rx) || !is_zero_to_one(ry) || + !is_zero_to_one(gx) || !is_zero_to_one(gy) || + !is_zero_to_one(bx) || !is_zero_to_one(by) || + !is_zero_to_one(wx) || !is_zero_to_one(wy) || + !toXYZD50) { + return false; + } + + // First, we need to convert xy values (primaries) to XYZ. + skcms_Matrix3x3 primaries = {{ + { rx, gx, bx }, + { ry, gy, by }, + { 1 - rx - ry, 1 - gx - gy, 1 - bx - by }, + }}; + skcms_Matrix3x3 primaries_inv; + if (!skcms_Matrix3x3_invert(&primaries, &primaries_inv)) { + return false; + } + + // Assumes that Y is 1.0f. + skcms_Vector3 wXYZ = { { wx / wy, 1, (1 - wx - wy) / wy } }; + skcms_Vector3 XYZ = mv_mul(&primaries_inv, &wXYZ); + + skcms_Matrix3x3 toXYZ = {{ + { XYZ.vals[0], 0, 0 }, + { 0, XYZ.vals[1], 0 }, + { 0, 0, XYZ.vals[2] }, + }}; + toXYZ = skcms_Matrix3x3_concat(&primaries, &toXYZ); + + skcms_Matrix3x3 DXtoD50; + if (!skcms_AdaptToXYZD50(wx, wy, &DXtoD50)) { + return false; + } + + *toXYZD50 = skcms_Matrix3x3_concat(&DXtoD50, &toXYZ); + return true; +} + + +bool skcms_Matrix3x3_invert(const skcms_Matrix3x3* src, skcms_Matrix3x3* dst) { + double a00 = src->vals[0][0], + a01 = src->vals[1][0], + a02 = src->vals[2][0], + a10 = src->vals[0][1], + a11 = src->vals[1][1], + a12 = src->vals[2][1], + a20 = src->vals[0][2], + a21 = src->vals[1][2], + a22 = src->vals[2][2]; + + double b0 = a00*a11 - a01*a10, + b1 = a00*a12 - a02*a10, + b2 = a01*a12 - a02*a11, + b3 = a20, + b4 = a21, + b5 = a22; + + double determinant = b0*b5 + - b1*b4 + + b2*b3; + + if (determinant == 0) { + return false; + } + + double invdet = 1.0 / determinant; + if (invdet > +FLT_MAX || invdet < -FLT_MAX || !isfinitef_((float)invdet)) { + return false; + } + + b0 *= invdet; + b1 *= invdet; + b2 *= invdet; + b3 *= invdet; + b4 *= invdet; + b5 *= invdet; + + dst->vals[0][0] = (float)( a11*b5 - a12*b4 ); + dst->vals[1][0] = (float)( a02*b4 - a01*b5 ); + dst->vals[2][0] = (float)( + b2 ); + dst->vals[0][1] = (float)( a12*b3 - a10*b5 ); + dst->vals[1][1] = (float)( a00*b5 - a02*b3 ); + dst->vals[2][1] = (float)( - b1 ); + dst->vals[0][2] = (float)( a10*b4 - a11*b3 ); + dst->vals[1][2] = (float)( a01*b3 - a00*b4 ); + dst->vals[2][2] = (float)( + b0 ); + + for (int r = 0; r < 3; ++r) + for (int c = 0; c < 3; ++c) { + if (!isfinitef_(dst->vals[r][c])) { + return false; + } + } + return true; +} + +skcms_Matrix3x3 skcms_Matrix3x3_concat(const skcms_Matrix3x3* A, const skcms_Matrix3x3* B) { + skcms_Matrix3x3 m = { { { 0,0,0 },{ 0,0,0 },{ 0,0,0 } } }; + for (int r = 0; r < 3; r++) + for (int c = 0; c < 3; c++) { + m.vals[r][c] = A->vals[r][0] * B->vals[0][c] + + A->vals[r][1] * B->vals[1][c] + + A->vals[r][2] * B->vals[2][c]; + } + return m; +} + +#if defined(__clang__) + [[clang::no_sanitize("float-divide-by-zero")]] // Checked for by classify() on the way out. +#endif +bool skcms_TransferFunction_invert(const skcms_TransferFunction* src, skcms_TransferFunction* dst) { + TF_PQish pq; + TF_HLGish hlg; + switch (classify(*src, &pq, &hlg)) { + case skcms_TFType_Invalid: return false; + case skcms_TFType_sRGBish: break; // handled below + + case skcms_TFType_PQish: + *dst = { TFKind_marker(skcms_TFType_PQish), -pq.A, pq.D, 1.0f/pq.F + , pq.B, -pq.E, 1.0f/pq.C}; + return true; + + case skcms_TFType_HLGish: + *dst = { TFKind_marker(skcms_TFType_HLGinvish), 1.0f/hlg.R, 1.0f/hlg.G + , 1.0f/hlg.a, hlg.b, hlg.c + , hlg.K_minus_1 }; + return true; + + case skcms_TFType_HLGinvish: + *dst = { TFKind_marker(skcms_TFType_HLGish), 1.0f/hlg.R, 1.0f/hlg.G + , 1.0f/hlg.a, hlg.b, hlg.c + , hlg.K_minus_1 }; + return true; + } + + assert (classify(*src) == skcms_TFType_sRGBish); + + // We're inverting this function, solving for x in terms of y. + // y = (cx + f) x < d + // (ax + b)^g + e x ≥ d + // The inverse of this function can be expressed in the same piecewise form. + skcms_TransferFunction inv = {0,0,0,0,0,0,0}; + + // We'll start by finding the new threshold inv.d. + // In principle we should be able to find that by solving for y at x=d from either side. + // (If those two d values aren't the same, it's a discontinuous transfer function.) + float d_l = src->c * src->d + src->f, + d_r = powf_(src->a * src->d + src->b, src->g) + src->e; + if (fabsf_(d_l - d_r) > 1/512.0f) { + return false; + } + inv.d = d_l; // TODO(mtklein): better in practice to choose d_r? + + // When d=0, the linear section collapses to a point. We leave c,d,f all zero in that case. + if (inv.d > 0) { + // Inverting the linear section is pretty straightfoward: + // y = cx + f + // y - f = cx + // (1/c)y - f/c = x + inv.c = 1.0f/src->c; + inv.f = -src->f/src->c; + } + + // The interesting part is inverting the nonlinear section: + // y = (ax + b)^g + e. + // y - e = (ax + b)^g + // (y - e)^1/g = ax + b + // (y - e)^1/g - b = ax + // (1/a)(y - e)^1/g - b/a = x + // + // To make that fit our form, we need to move the (1/a) term inside the exponentiation: + // let k = (1/a)^g + // (1/a)( y - e)^1/g - b/a = x + // (ky - ke)^1/g - b/a = x + + float k = powf_(src->a, -src->g); // (1/a)^g == a^-g + inv.g = 1.0f / src->g; + inv.a = k; + inv.b = -k * src->e; + inv.e = -src->b / src->a; + + // We need to enforce the same constraints here that we do when fitting a curve, + // a >= 0 and ad+b >= 0. These constraints are checked by classify(), so they're true + // of the source function if we're here. + + // Just like when fitting the curve, there's really no way to rescue a < 0. + if (inv.a < 0) { + return false; + } + // On the other hand we can rescue an ad+b that's gone slightly negative here. + if (inv.a * inv.d + inv.b < 0) { + inv.b = -inv.a * inv.d; + } + + // That should usually make classify(inv) == sRGBish true, but there are a couple situations + // where we might still fail here, like non-finite parameter values. + if (classify(inv) != skcms_TFType_sRGBish) { + return false; + } + + assert (inv.a >= 0); + assert (inv.a * inv.d + inv.b >= 0); + + // Now in principle we're done. + // But to preserve the valuable invariant inv(src(1.0f)) == 1.0f, we'll tweak + // e or f of the inverse, depending on which segment contains src(1.0f). + float s = skcms_TransferFunction_eval(src, 1.0f); + if (!isfinitef_(s)) { + return false; + } + + float sign = s < 0 ? -1.0f : 1.0f; + s *= sign; + if (s < inv.d) { + inv.f = 1.0f - sign * inv.c * s; + } else { + inv.e = 1.0f - sign * powf_(inv.a * s + inv.b, inv.g); + } + + *dst = inv; + return classify(*dst) == skcms_TFType_sRGBish; +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // + +// From here below we're approximating an skcms_Curve with an skcms_TransferFunction{g,a,b,c,d,e,f}: +// +// tf(x) = cx + f x < d +// tf(x) = (ax + b)^g + e x ≥ d +// +// When fitting, we add the additional constraint that both pieces meet at d: +// +// cd + f = (ad + b)^g + e +// +// Solving for e and folding it through gives an alternate formulation of the non-linear piece: +// +// tf(x) = cx + f x < d +// tf(x) = (ax + b)^g - (ad + b)^g + cd + f x ≥ d +// +// Our overall strategy is then: +// For a couple tolerances, +// - fit_linear(): fit c,d,f iteratively to as many points as our tolerance allows +// - invert c,d,f +// - fit_nonlinear(): fit g,a,b using Gauss-Newton given those inverted c,d,f +// (and by constraint, inverted e) to the inverse of the table. +// Return the parameters with least maximum error. +// +// To run Gauss-Newton to find g,a,b, we'll also need the gradient of the residuals +// of round-trip f_inv(x), the inverse of the non-linear piece of f(x). +// +// let y = Table(x) +// r(x) = x - f_inv(y) +// +// ∂r/∂g = ln(ay + b)*(ay + b)^g +// - ln(ad + b)*(ad + b)^g +// ∂r/∂a = yg(ay + b)^(g-1) +// - dg(ad + b)^(g-1) +// ∂r/∂b = g(ay + b)^(g-1) +// - g(ad + b)^(g-1) + +// Return the residual of roundtripping skcms_Curve(x) through f_inv(y) with parameters P, +// and fill out the gradient of the residual into dfdP. +static float rg_nonlinear(float x, + const skcms_Curve* curve, + const skcms_TransferFunction* tf, + float dfdP[3]) { + const float y = eval_curve(curve, x); + + const float g = tf->g, a = tf->a, b = tf->b, + c = tf->c, d = tf->d, f = tf->f; + + const float Y = fmaxf_(a*y + b, 0.0f), + D = a*d + b; + assert (D >= 0); + + // The gradient. + dfdP[0] = logf_(Y)*powf_(Y, g) + - logf_(D)*powf_(D, g); + dfdP[1] = y*g*powf_(Y, g-1) + - d*g*powf_(D, g-1); + dfdP[2] = g*powf_(Y, g-1) + - g*powf_(D, g-1); + + // The residual. + const float f_inv = powf_(Y, g) + - powf_(D, g) + + c*d + f; + return x - f_inv; +} + +static bool gauss_newton_step(const skcms_Curve* curve, + skcms_TransferFunction* tf, + float x0, float dx, int N) { + // We'll sample x from the range [x0,x1] (both inclusive) N times with even spacing. + // + // Let P = [ tf->g, tf->a, tf->b ] (the three terms that we're adjusting). + // + // We want to do P' = P + (Jf^T Jf)^-1 Jf^T r(P), + // where r(P) is the residual vector + // and Jf is the Jacobian matrix of f(), ∂r/∂P. + // + // Let's review the shape of each of these expressions: + // r(P) is [N x 1], a column vector with one entry per value of x tested + // Jf is [N x 3], a matrix with an entry for each (x,P) pair + // Jf^T is [3 x N], the transpose of Jf + // + // Jf^T Jf is [3 x N] * [N x 3] == [3 x 3], a 3x3 matrix, + // and so is its inverse (Jf^T Jf)^-1 + // Jf^T r(P) is [3 x N] * [N x 1] == [3 x 1], a column vector with the same shape as P + // + // Our implementation strategy to get to the final ∆P is + // 1) evaluate Jf^T Jf, call that lhs + // 2) evaluate Jf^T r(P), call that rhs + // 3) invert lhs + // 4) multiply inverse lhs by rhs + // + // This is a friendly implementation strategy because we don't have to have any + // buffers that scale with N, and equally nice don't have to perform any matrix + // operations that are variable size. + // + // Other implementation strategies could trade this off, e.g. evaluating the + // pseudoinverse of Jf ( (Jf^T Jf)^-1 Jf^T ) directly, then multiplying that by + // the residuals. That would probably require implementing singular value + // decomposition, and would create a [3 x N] matrix to be multiplied by the + // [N x 1] residual vector, but on the upside I think that'd eliminate the + // possibility of this gauss_newton_step() function ever failing. + + // 0) start off with lhs and rhs safely zeroed. + skcms_Matrix3x3 lhs = {{ {0,0,0}, {0,0,0}, {0,0,0} }}; + skcms_Vector3 rhs = { {0,0,0} }; + + // 1,2) evaluate lhs and evaluate rhs + // We want to evaluate Jf only once, but both lhs and rhs involve Jf^T, + // so we'll have to update lhs and rhs at the same time. + for (int i = 0; i < N; i++) { + float x = x0 + static_cast(i)*dx; + + float dfdP[3] = {0,0,0}; + float resid = rg_nonlinear(x,curve,tf, dfdP); + + for (int r = 0; r < 3; r++) { + for (int c = 0; c < 3; c++) { + lhs.vals[r][c] += dfdP[r] * dfdP[c]; + } + rhs.vals[r] += dfdP[r] * resid; + } + } + + // If any of the 3 P parameters are unused, this matrix will be singular. + // Detect those cases and fix them up to indentity instead, so we can invert. + for (int k = 0; k < 3; k++) { + if (lhs.vals[0][k]==0 && lhs.vals[1][k]==0 && lhs.vals[2][k]==0 && + lhs.vals[k][0]==0 && lhs.vals[k][1]==0 && lhs.vals[k][2]==0) { + lhs.vals[k][k] = 1; + } + } + + // 3) invert lhs + skcms_Matrix3x3 lhs_inv; + if (!skcms_Matrix3x3_invert(&lhs, &lhs_inv)) { + return false; + } + + // 4) multiply inverse lhs by rhs + skcms_Vector3 dP = mv_mul(&lhs_inv, &rhs); + tf->g += dP.vals[0]; + tf->a += dP.vals[1]; + tf->b += dP.vals[2]; + return isfinitef_(tf->g) && isfinitef_(tf->a) && isfinitef_(tf->b); +} + +static float max_roundtrip_error_checked(const skcms_Curve* curve, + const skcms_TransferFunction* tf_inv) { + skcms_TransferFunction tf; + if (!skcms_TransferFunction_invert(tf_inv, &tf) || skcms_TFType_sRGBish != classify(tf)) { + return INFINITY_; + } + + skcms_TransferFunction tf_inv_again; + if (!skcms_TransferFunction_invert(&tf, &tf_inv_again)) { + return INFINITY_; + } + + return skcms_MaxRoundtripError(curve, &tf_inv_again); +} + +// Fit the points in [L,N) to the non-linear piece of tf, or return false if we can't. +static bool fit_nonlinear(const skcms_Curve* curve, int L, int N, skcms_TransferFunction* tf) { + // This enforces a few constraints that are not modeled in gauss_newton_step()'s optimization. + auto fixup_tf = [tf]() { + // a must be non-negative. That ensures the function is monotonically increasing. + // We don't really know how to fix up a if it goes negative. + if (tf->a < 0) { + return false; + } + // ad+b must be non-negative. That ensures we don't end up with complex numbers in powf. + // We feel just barely not uneasy enough to tweak b so ad+b is zero in this case. + if (tf->a * tf->d + tf->b < 0) { + tf->b = -tf->a * tf->d; + } + assert (tf->a >= 0 && + tf->a * tf->d + tf->b >= 0); + + // cd+f must be ~= (ad+b)^g+e. That ensures the function is continuous. We keep e as a free + // parameter so we can guarantee this. + tf->e = tf->c*tf->d + tf->f + - powf_(tf->a*tf->d + tf->b, tf->g); + + return isfinitef_(tf->e); + }; + + if (!fixup_tf()) { + return false; + } + + // No matter where we start, dx should always represent N even steps from 0 to 1. + const float dx = 1.0f / static_cast(N-1); + + skcms_TransferFunction best_tf = *tf; + float best_max_error = INFINITY_; + + // Need this or several curves get worse... *sigh* + float init_error = max_roundtrip_error_checked(curve, tf); + if (init_error < best_max_error) { + best_max_error = init_error; + best_tf = *tf; + } + + // As far as we can tell, 1 Gauss-Newton step won't converge, and 3 steps is no better than 2. + for (int j = 0; j < 8; j++) { + if (!gauss_newton_step(curve, tf, static_cast(L)*dx, dx, N-L) || !fixup_tf()) { + *tf = best_tf; + return isfinitef_(best_max_error); + } + + float max_error = max_roundtrip_error_checked(curve, tf); + if (max_error < best_max_error) { + best_max_error = max_error; + best_tf = *tf; + } + } + + *tf = best_tf; + return isfinitef_(best_max_error); +} + +bool skcms_ApproximateCurve(const skcms_Curve* curve, + skcms_TransferFunction* approx, + float* max_error) { + if (!curve || !approx || !max_error) { + return false; + } + + if (curve->table_entries == 0) { + // No point approximating an skcms_TransferFunction with an skcms_TransferFunction! + return false; + } + + if (curve->table_entries == 1 || curve->table_entries > (uint32_t)INT_MAX) { + // We need at least two points, and must put some reasonable cap on the maximum number. + return false; + } + + int N = (int)curve->table_entries; + const float dx = 1.0f / static_cast(N - 1); + + *max_error = INFINITY_; + const float kTolerances[] = { 1.5f / 65535.0f, 1.0f / 512.0f }; + for (int t = 0; t < ARRAY_COUNT(kTolerances); t++) { + skcms_TransferFunction tf, + tf_inv; + + // It's problematic to fit curves with non-zero f, so always force it to zero explicitly. + tf.f = 0.0f; + int L = fit_linear(curve, N, kTolerances[t], &tf.c, &tf.d); + + if (L == N) { + // If the entire data set was linear, move the coefficients to the nonlinear portion + // with G == 1. This lets use a canonical representation with d == 0. + tf.g = 1; + tf.a = tf.c; + tf.b = tf.f; + tf.c = tf.d = tf.e = tf.f = 0; + } else if (L == N - 1) { + // Degenerate case with only two points in the nonlinear segment. Solve directly. + tf.g = 1; + tf.a = (eval_curve(curve, static_cast(N-1)*dx) - + eval_curve(curve, static_cast(N-2)*dx)) + / dx; + tf.b = eval_curve(curve, static_cast(N-2)*dx) + - tf.a * static_cast(N-2)*dx; + tf.e = 0; + } else { + // Start by guessing a gamma-only curve through the midpoint. + int mid = (L + N) / 2; + float mid_x = static_cast(mid) / static_cast(N - 1); + float mid_y = eval_curve(curve, mid_x); + tf.g = log2f_(mid_y) / log2f_(mid_x); + tf.a = 1; + tf.b = 0; + tf.e = tf.c*tf.d + tf.f + - powf_(tf.a*tf.d + tf.b, tf.g); + + + if (!skcms_TransferFunction_invert(&tf, &tf_inv) || + !fit_nonlinear(curve, L,N, &tf_inv)) { + continue; + } + + // We fit tf_inv, so calculate tf to keep in sync. + // fit_nonlinear() should guarantee invertibility. + if (!skcms_TransferFunction_invert(&tf_inv, &tf)) { + assert(false); + continue; + } + } + + // We'd better have a sane, sRGB-ish TF by now. + // Other non-Bad TFs would be fine, but we know we've only ever tried to fit sRGBish; + // anything else is just some accident of math and the way we pun tf.g as a type flag. + // fit_nonlinear() should guarantee this, but the special cases may fail this test. + if (skcms_TFType_sRGBish != classify(tf)) { + continue; + } + + // We find our error by roundtripping the table through tf_inv. + // + // (The most likely use case for this approximation is to be inverted and + // used as the transfer function for a destination color space.) + // + // We've kept tf and tf_inv in sync above, but we can't guarantee that tf is + // invertible, so re-verify that here (and use the new inverse for testing). + // fit_nonlinear() should guarantee this, but the special cases that don't use + // it may fail this test. + if (!skcms_TransferFunction_invert(&tf, &tf_inv)) { + continue; + } + + float err = skcms_MaxRoundtripError(curve, &tf_inv); + if (*max_error > err) { + *max_error = err; + *approx = tf; + } + } + return isfinitef_(*max_error); +} + +enum class CpuType { Baseline, HSW, SKX }; + +static CpuType cpu_type() { + #if defined(SKCMS_PORTABLE) || !defined(__x86_64__) || defined(SKCMS_FORCE_BASELINE) + return CpuType::Baseline; + #elif defined(SKCMS_FORCE_HSW) + return CpuType::HSW; + #elif defined(SKCMS_FORCE_SKX) + return CpuType::SKX; + #else + static const CpuType type = []{ + if (!sAllowRuntimeCPUDetection) { + return CpuType::Baseline; + } + // See http://www.sandpile.org/x86/cpuid.htm + + // First, a basic cpuid(1) lets us check prerequisites for HSW, SKX. + uint32_t eax, ebx, ecx, edx; + __asm__ __volatile__("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) + : "0"(1), "2"(0)); + if ((edx & (1u<<25)) && // SSE + (edx & (1u<<26)) && // SSE2 + (ecx & (1u<< 0)) && // SSE3 + (ecx & (1u<< 9)) && // SSSE3 + (ecx & (1u<<12)) && // FMA (N.B. not used, avoided even) + (ecx & (1u<<19)) && // SSE4.1 + (ecx & (1u<<20)) && // SSE4.2 + (ecx & (1u<<26)) && // XSAVE + (ecx & (1u<<27)) && // OSXSAVE + (ecx & (1u<<28)) && // AVX + (ecx & (1u<<29))) { // F16C + + // Call cpuid(7) to check for AVX2 and AVX-512 bits. + __asm__ __volatile__("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) + : "0"(7), "2"(0)); + // eax from xgetbv(0) will tell us whether XMM, YMM, and ZMM state is saved. + uint32_t xcr0, dont_need_edx; + __asm__ __volatile__("xgetbv" : "=a"(xcr0), "=d"(dont_need_edx) : "c"(0)); + + if ((xcr0 & (1u<<1)) && // XMM register state saved? + (xcr0 & (1u<<2)) && // YMM register state saved? + (ebx & (1u<<5))) { // AVX2 + // At this point we're at least HSW. Continue checking for SKX. + if ((xcr0 & (1u<< 5)) && // Opmasks state saved? + (xcr0 & (1u<< 6)) && // First 16 ZMM registers saved? + (xcr0 & (1u<< 7)) && // High 16 ZMM registers saved? + (ebx & (1u<<16)) && // AVX512F + (ebx & (1u<<17)) && // AVX512DQ + (ebx & (1u<<28)) && // AVX512CD + (ebx & (1u<<30)) && // AVX512BW + (ebx & (1u<<31))) { // AVX512VL + return CpuType::SKX; + } + return CpuType::HSW; + } + } + return CpuType::Baseline; + }(); + return type; + #endif +} + +static bool tf_is_gamma(const skcms_TransferFunction& tf) { + return tf.g > 0 && tf.a == 1 && + tf.b == 0 && tf.c == 0 && tf.d == 0 && tf.e == 0 && tf.f == 0; +} + +struct OpAndArg { + Op op; + const void* arg; +}; + +static OpAndArg select_curve_op(const skcms_Curve* curve, int channel) { + struct OpType { + Op sGamma, sRGBish, PQish, HLGish, HLGinvish, table; + }; + static constexpr OpType kOps[] = { + { Op::gamma_r, Op::tf_r, Op::pq_r, Op::hlg_r, Op::hlginv_r, Op::table_r }, + { Op::gamma_g, Op::tf_g, Op::pq_g, Op::hlg_g, Op::hlginv_g, Op::table_g }, + { Op::gamma_b, Op::tf_b, Op::pq_b, Op::hlg_b, Op::hlginv_b, Op::table_b }, + { Op::gamma_a, Op::tf_a, Op::pq_a, Op::hlg_a, Op::hlginv_a, Op::table_a }, + }; + const auto& op = kOps[channel]; + + if (curve->table_entries == 0) { + const OpAndArg noop = { Op::load_a8/*doesn't matter*/, nullptr }; + + const skcms_TransferFunction& tf = curve->parametric; + + if (tf_is_gamma(tf)) { + return tf.g != 1 ? OpAndArg{op.sGamma, &tf} + : noop; + } + + switch (classify(tf)) { + case skcms_TFType_Invalid: return noop; + case skcms_TFType_sRGBish: return OpAndArg{op.sRGBish, &tf}; + case skcms_TFType_PQish: return OpAndArg{op.PQish, &tf}; + case skcms_TFType_HLGish: return OpAndArg{op.HLGish, &tf}; + case skcms_TFType_HLGinvish: return OpAndArg{op.HLGinvish, &tf}; + } + } + return OpAndArg{op.table, curve}; +} + +static int select_curve_ops(const skcms_Curve* curves, int numChannels, OpAndArg* ops) { + // We process the channels in reverse order, yielding ops in ABGR order. + // (Working backwards allows us to fuse trailing B+G+R ops into a single RGB op.) + int cursor = 0; + for (int index = numChannels; index-- > 0; ) { + ops[cursor] = select_curve_op(&curves[index], index); + if (ops[cursor].arg) { + ++cursor; + } + } + + // Identify separate B+G+R ops and fuse them into a single RGB op. + if (cursor >= 3) { + struct FusableOps { + Op r, g, b, rgb; + }; + static constexpr FusableOps kFusableOps[] = { + {Op::gamma_r, Op::gamma_g, Op::gamma_b, Op::gamma_rgb}, + {Op::tf_r, Op::tf_g, Op::tf_b, Op::tf_rgb}, + {Op::pq_r, Op::pq_g, Op::pq_b, Op::pq_rgb}, + {Op::hlg_r, Op::hlg_g, Op::hlg_b, Op::hlg_rgb}, + {Op::hlginv_r, Op::hlginv_g, Op::hlginv_b, Op::hlginv_rgb}, + }; + + int posR = cursor - 1; + int posG = cursor - 2; + int posB = cursor - 3; + for (const FusableOps& fusableOp : kFusableOps) { + if (ops[posR].op == fusableOp.r && + ops[posG].op == fusableOp.g && + ops[posB].op == fusableOp.b && + (0 == memcmp(ops[posR].arg, ops[posG].arg, sizeof(skcms_TransferFunction))) && + (0 == memcmp(ops[posR].arg, ops[posB].arg, sizeof(skcms_TransferFunction)))) { + // Fuse the three matching ops into one. + ops[posB].op = fusableOp.rgb; + cursor -= 2; + break; + } + } + } + + return cursor; +} + +static size_t bytes_per_pixel(skcms_PixelFormat fmt) { + switch (fmt >> 1) { // ignore rgb/bgr + case skcms_PixelFormat_A_8 >> 1: return 1; + case skcms_PixelFormat_G_8 >> 1: return 1; + case skcms_PixelFormat_ABGR_4444 >> 1: return 2; + case skcms_PixelFormat_RGB_565 >> 1: return 2; + case skcms_PixelFormat_RGB_888 >> 1: return 3; + case skcms_PixelFormat_RGBA_8888 >> 1: return 4; + case skcms_PixelFormat_RGBA_8888_sRGB >> 1: return 4; + case skcms_PixelFormat_RGBA_1010102 >> 1: return 4; + case skcms_PixelFormat_RGB_101010x_XR >> 1: return 4; + case skcms_PixelFormat_RGB_161616LE >> 1: return 6; + case skcms_PixelFormat_RGBA_10101010_XR >> 1: return 8; + case skcms_PixelFormat_RGBA_16161616LE >> 1: return 8; + case skcms_PixelFormat_RGB_161616BE >> 1: return 6; + case skcms_PixelFormat_RGBA_16161616BE >> 1: return 8; + case skcms_PixelFormat_RGB_hhh_Norm >> 1: return 6; + case skcms_PixelFormat_RGBA_hhhh_Norm >> 1: return 8; + case skcms_PixelFormat_RGB_hhh >> 1: return 6; + case skcms_PixelFormat_RGBA_hhhh >> 1: return 8; + case skcms_PixelFormat_RGB_fff >> 1: return 12; + case skcms_PixelFormat_RGBA_ffff >> 1: return 16; + } + assert(false); + return 0; +} + +static bool prep_for_destination(const skcms_ICCProfile* profile, + skcms_Matrix3x3* fromXYZD50, + skcms_TransferFunction* invR, + skcms_TransferFunction* invG, + skcms_TransferFunction* invB) { + // skcms_Transform() supports B2A destinations... + if (profile->has_B2A) { return true; } + // ...and destinations with parametric transfer functions and an XYZD50 gamut matrix. + return profile->has_trc + && profile->has_toXYZD50 + && profile->trc[0].table_entries == 0 + && profile->trc[1].table_entries == 0 + && profile->trc[2].table_entries == 0 + && skcms_TransferFunction_invert(&profile->trc[0].parametric, invR) + && skcms_TransferFunction_invert(&profile->trc[1].parametric, invG) + && skcms_TransferFunction_invert(&profile->trc[2].parametric, invB) + && skcms_Matrix3x3_invert(&profile->toXYZD50, fromXYZD50); +} + +bool skcms_Transform(const void* src, + skcms_PixelFormat srcFmt, + skcms_AlphaFormat srcAlpha, + const skcms_ICCProfile* srcProfile, + void* dst, + skcms_PixelFormat dstFmt, + skcms_AlphaFormat dstAlpha, + const skcms_ICCProfile* dstProfile, + size_t nz) { + const size_t dst_bpp = bytes_per_pixel(dstFmt), + src_bpp = bytes_per_pixel(srcFmt); + // Let's just refuse if the request is absurdly big. + if (nz * dst_bpp > INT_MAX || nz * src_bpp > INT_MAX) { + return false; + } + int n = (int)nz; + + // Null profiles default to sRGB. Passing null for both is handy when doing format conversion. + if (!srcProfile) { + srcProfile = skcms_sRGB_profile(); + } + if (!dstProfile) { + dstProfile = skcms_sRGB_profile(); + } + + // We can't transform in place unless the PixelFormats are the same size. + if (dst == src && dst_bpp != src_bpp) { + return false; + } + // TODO: more careful alias rejection (like, dst == src + 1)? + + Op program[32]; + const void* context[32]; + + Op* ops = program; + const void** contexts = context; + + auto add_op = [&](Op o) { + *ops++ = o; + *contexts++ = nullptr; + }; + + auto add_op_ctx = [&](Op o, const void* c) { + *ops++ = o; + *contexts++ = c; + }; + + auto add_curve_ops = [&](const skcms_Curve* curves, int numChannels) { + OpAndArg oa[4]; + assert(numChannels <= ARRAY_COUNT(oa)); + + int numOps = select_curve_ops(curves, numChannels, oa); + + for (int i = 0; i < numOps; ++i) { + add_op_ctx(oa[i].op, oa[i].arg); + } + }; + + // These are always parametric curves of some sort. + skcms_Curve dst_curves[3]; + dst_curves[0].table_entries = + dst_curves[1].table_entries = + dst_curves[2].table_entries = 0; + + skcms_Matrix3x3 from_xyz; + + switch (srcFmt >> 1) { + default: return false; + case skcms_PixelFormat_A_8 >> 1: add_op(Op::load_a8); break; + case skcms_PixelFormat_G_8 >> 1: add_op(Op::load_g8); break; + case skcms_PixelFormat_ABGR_4444 >> 1: add_op(Op::load_4444); break; + case skcms_PixelFormat_RGB_565 >> 1: add_op(Op::load_565); break; + case skcms_PixelFormat_RGB_888 >> 1: add_op(Op::load_888); break; + case skcms_PixelFormat_RGBA_8888 >> 1: add_op(Op::load_8888); break; + case skcms_PixelFormat_RGBA_1010102 >> 1: add_op(Op::load_1010102); break; + case skcms_PixelFormat_RGB_101010x_XR >> 1: add_op(Op::load_101010x_XR); break; + case skcms_PixelFormat_RGBA_10101010_XR >> 1: add_op(Op::load_10101010_XR); break; + case skcms_PixelFormat_RGB_161616LE >> 1: add_op(Op::load_161616LE); break; + case skcms_PixelFormat_RGBA_16161616LE >> 1: add_op(Op::load_16161616LE); break; + case skcms_PixelFormat_RGB_161616BE >> 1: add_op(Op::load_161616BE); break; + case skcms_PixelFormat_RGBA_16161616BE >> 1: add_op(Op::load_16161616BE); break; + case skcms_PixelFormat_RGB_hhh_Norm >> 1: add_op(Op::load_hhh); break; + case skcms_PixelFormat_RGBA_hhhh_Norm >> 1: add_op(Op::load_hhhh); break; + case skcms_PixelFormat_RGB_hhh >> 1: add_op(Op::load_hhh); break; + case skcms_PixelFormat_RGBA_hhhh >> 1: add_op(Op::load_hhhh); break; + case skcms_PixelFormat_RGB_fff >> 1: add_op(Op::load_fff); break; + case skcms_PixelFormat_RGBA_ffff >> 1: add_op(Op::load_ffff); break; + + case skcms_PixelFormat_RGBA_8888_sRGB >> 1: + add_op(Op::load_8888); + add_op_ctx(Op::tf_rgb, skcms_sRGB_TransferFunction()); + break; + } + if (srcFmt == skcms_PixelFormat_RGB_hhh_Norm || + srcFmt == skcms_PixelFormat_RGBA_hhhh_Norm) { + add_op(Op::clamp); + } + if (srcFmt & 1) { + add_op(Op::swap_rb); + } + skcms_ICCProfile gray_dst_profile; + if ((dstFmt >> 1) == (skcms_PixelFormat_G_8 >> 1)) { + // When transforming to gray, stop at XYZ (by setting toXYZ to identity), then transform + // luminance (Y) by the destination transfer function. + gray_dst_profile = *dstProfile; + skcms_SetXYZD50(&gray_dst_profile, &skcms_XYZD50_profile()->toXYZD50); + dstProfile = &gray_dst_profile; + } + + if (srcProfile->data_color_space == skcms_Signature_CMYK) { + // Photoshop creates CMYK images as inverse CMYK. + // These happen to be the only ones we've _ever_ seen. + add_op(Op::invert); + // With CMYK, ignore the alpha type, to avoid changing K or conflating CMY with K. + srcAlpha = skcms_AlphaFormat_Unpremul; + } + + if (srcAlpha == skcms_AlphaFormat_Opaque) { + add_op(Op::force_opaque); + } else if (srcAlpha == skcms_AlphaFormat_PremulAsEncoded) { + add_op(Op::unpremul); + } + + if (dstProfile != srcProfile) { + + if (!prep_for_destination(dstProfile, + &from_xyz, + &dst_curves[0].parametric, + &dst_curves[1].parametric, + &dst_curves[2].parametric)) { + return false; + } + + if (srcProfile->has_A2B) { + if (srcProfile->A2B.input_channels) { + add_curve_ops(srcProfile->A2B.input_curves, + (int)srcProfile->A2B.input_channels); + add_op(Op::clamp); + add_op_ctx(Op::clut_A2B, &srcProfile->A2B); + } + + if (srcProfile->A2B.matrix_channels == 3) { + add_curve_ops(srcProfile->A2B.matrix_curves, /*numChannels=*/3); + + static const skcms_Matrix3x4 I = {{ + {1,0,0,0}, + {0,1,0,0}, + {0,0,1,0}, + }}; + if (0 != memcmp(&I, &srcProfile->A2B.matrix, sizeof(I))) { + add_op_ctx(Op::matrix_3x4, &srcProfile->A2B.matrix); + } + } + + if (srcProfile->A2B.output_channels == 3) { + add_curve_ops(srcProfile->A2B.output_curves, /*numChannels=*/3); + } + + if (srcProfile->pcs == skcms_Signature_Lab) { + add_op(Op::lab_to_xyz); + } + + } else if (srcProfile->has_trc && srcProfile->has_toXYZD50) { + add_curve_ops(srcProfile->trc, /*numChannels=*/3); + } else { + return false; + } + + // A2B sources are in XYZD50 by now, but TRC sources are still in their original gamut. + assert (srcProfile->has_A2B || srcProfile->has_toXYZD50); + + if (dstProfile->has_B2A) { + // B2A needs its input in XYZD50, so transform TRC sources now. + if (!srcProfile->has_A2B) { + add_op_ctx(Op::matrix_3x3, &srcProfile->toXYZD50); + } + + if (dstProfile->pcs == skcms_Signature_Lab) { + add_op(Op::xyz_to_lab); + } + + if (dstProfile->B2A.input_channels == 3) { + add_curve_ops(dstProfile->B2A.input_curves, /*numChannels=*/3); + } + + if (dstProfile->B2A.matrix_channels == 3) { + static const skcms_Matrix3x4 I = {{ + {1,0,0,0}, + {0,1,0,0}, + {0,0,1,0}, + }}; + if (0 != memcmp(&I, &dstProfile->B2A.matrix, sizeof(I))) { + add_op_ctx(Op::matrix_3x4, &dstProfile->B2A.matrix); + } + + add_curve_ops(dstProfile->B2A.matrix_curves, /*numChannels=*/3); + } + + if (dstProfile->B2A.output_channels) { + add_op(Op::clamp); + add_op_ctx(Op::clut_B2A, &dstProfile->B2A); + + add_curve_ops(dstProfile->B2A.output_curves, + (int)dstProfile->B2A.output_channels); + } + } else { + // This is a TRC destination. + // We'll concat any src->xyz matrix with our xyz->dst matrix into one src->dst matrix. + // (A2B sources are already in XYZD50, making that src->xyz matrix I.) + static const skcms_Matrix3x3 I = {{ + { 1.0f, 0.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f }, + }}; + const skcms_Matrix3x3* to_xyz = srcProfile->has_A2B ? &I : &srcProfile->toXYZD50; + + // There's a chance the source and destination gamuts are identical, + // in which case we can skip the gamut transform. + if (0 != memcmp(&dstProfile->toXYZD50, to_xyz, sizeof(skcms_Matrix3x3))) { + // Concat the entire gamut transform into from_xyz, + // now slightly misnamed but it's a handy spot to stash the result. + from_xyz = skcms_Matrix3x3_concat(&from_xyz, to_xyz); + add_op_ctx(Op::matrix_3x3, &from_xyz); + } + + // Encode back to dst RGB using its parametric transfer functions. + OpAndArg oa[3]; + int numOps = select_curve_ops(dst_curves, /*numChannels=*/3, oa); + for (int index = 0; index < numOps; ++index) { + assert(oa[index].op != Op::table_r && + oa[index].op != Op::table_g && + oa[index].op != Op::table_b && + oa[index].op != Op::table_a); + add_op_ctx(oa[index].op, oa[index].arg); + } + } + } + + // Clamp here before premul to make sure we're clamping to normalized values _and_ gamut, + // not just to values that fit in [0,1]. + // + // E.g. r = 1.1, a = 0.5 would fit fine in fixed point after premul (ra=0.55,a=0.5), + // but would be carrying r > 1, which is really unexpected for downstream consumers. + if (dstFmt < skcms_PixelFormat_RGB_hhh) { + add_op(Op::clamp); + } + + if (dstProfile->data_color_space == skcms_Signature_CMYK) { + // Photoshop creates CMYK images as inverse CMYK. + // These happen to be the only ones we've _ever_ seen. + add_op(Op::invert); + + // CMYK has no alpha channel, so make sure dstAlpha is a no-op. + dstAlpha = skcms_AlphaFormat_Unpremul; + } + + if (dstAlpha == skcms_AlphaFormat_Opaque) { + add_op(Op::force_opaque); + } else if (dstAlpha == skcms_AlphaFormat_PremulAsEncoded) { + add_op(Op::premul); + } + if (dstFmt & 1) { + add_op(Op::swap_rb); + } + switch (dstFmt >> 1) { + default: return false; + case skcms_PixelFormat_A_8 >> 1: add_op(Op::store_a8); break; + case skcms_PixelFormat_G_8 >> 1: add_op(Op::store_g8); break; + case skcms_PixelFormat_ABGR_4444 >> 1: add_op(Op::store_4444); break; + case skcms_PixelFormat_RGB_565 >> 1: add_op(Op::store_565); break; + case skcms_PixelFormat_RGB_888 >> 1: add_op(Op::store_888); break; + case skcms_PixelFormat_RGBA_8888 >> 1: add_op(Op::store_8888); break; + case skcms_PixelFormat_RGBA_1010102 >> 1: add_op(Op::store_1010102); break; + case skcms_PixelFormat_RGB_161616LE >> 1: add_op(Op::store_161616LE); break; + case skcms_PixelFormat_RGBA_16161616LE >> 1: add_op(Op::store_16161616LE); break; + case skcms_PixelFormat_RGB_161616BE >> 1: add_op(Op::store_161616BE); break; + case skcms_PixelFormat_RGBA_16161616BE >> 1: add_op(Op::store_16161616BE); break; + case skcms_PixelFormat_RGB_hhh_Norm >> 1: add_op(Op::store_hhh); break; + case skcms_PixelFormat_RGBA_hhhh_Norm >> 1: add_op(Op::store_hhhh); break; + case skcms_PixelFormat_RGB_101010x_XR >> 1: add_op(Op::store_101010x_XR); break; + case skcms_PixelFormat_RGB_hhh >> 1: add_op(Op::store_hhh); break; + case skcms_PixelFormat_RGBA_hhhh >> 1: add_op(Op::store_hhhh); break; + case skcms_PixelFormat_RGB_fff >> 1: add_op(Op::store_fff); break; + case skcms_PixelFormat_RGBA_ffff >> 1: add_op(Op::store_ffff); break; + + case skcms_PixelFormat_RGBA_8888_sRGB >> 1: + add_op_ctx(Op::tf_rgb, skcms_sRGB_Inverse_TransferFunction()); + add_op(Op::store_8888); + break; + } + + assert(ops <= program + ARRAY_COUNT(program)); + assert(contexts <= context + ARRAY_COUNT(context)); + + auto run = baseline::run_program; + switch (cpu_type()) { + case CpuType::SKX: + #if !defined(SKCMS_DISABLE_SKX) + run = skx::run_program; + break; + #endif + + case CpuType::HSW: + #if !defined(SKCMS_DISABLE_HSW) + run = hsw::run_program; + break; + #endif + + case CpuType::Baseline: + break; + } + + run(program, context, ops - program, (const char*)src, (char*)dst, n, src_bpp,dst_bpp); + return true; +} + +static void assert_usable_as_destination(const skcms_ICCProfile* profile) { +#if defined(NDEBUG) + (void)profile; +#else + skcms_Matrix3x3 fromXYZD50; + skcms_TransferFunction invR, invG, invB; + assert(prep_for_destination(profile, &fromXYZD50, &invR, &invG, &invB)); +#endif +} + +bool skcms_MakeUsableAsDestination(skcms_ICCProfile* profile) { + if (!profile->has_B2A) { + skcms_Matrix3x3 fromXYZD50; + if (!profile->has_trc || !profile->has_toXYZD50 + || !skcms_Matrix3x3_invert(&profile->toXYZD50, &fromXYZD50)) { + return false; + } + + skcms_TransferFunction tf[3]; + for (int i = 0; i < 3; i++) { + skcms_TransferFunction inv; + if (profile->trc[i].table_entries == 0 + && skcms_TransferFunction_invert(&profile->trc[i].parametric, &inv)) { + tf[i] = profile->trc[i].parametric; + continue; + } + + float max_error; + // Parametric curves from skcms_ApproximateCurve() are guaranteed to be invertible. + if (!skcms_ApproximateCurve(&profile->trc[i], &tf[i], &max_error)) { + return false; + } + } + + for (int i = 0; i < 3; ++i) { + profile->trc[i].table_entries = 0; + profile->trc[i].parametric = tf[i]; + } + } + assert_usable_as_destination(profile); + return true; +} + +bool skcms_MakeUsableAsDestinationWithSingleCurve(skcms_ICCProfile* profile) { + // Call skcms_MakeUsableAsDestination() with B2A disabled; + // on success that'll return a TRC/XYZ profile with three skcms_TransferFunctions. + skcms_ICCProfile result = *profile; + result.has_B2A = false; + if (!skcms_MakeUsableAsDestination(&result)) { + return false; + } + + // Of the three, pick the transfer function that best fits the other two. + int best_tf = 0; + float min_max_error = INFINITY_; + for (int i = 0; i < 3; i++) { + skcms_TransferFunction inv; + if (!skcms_TransferFunction_invert(&result.trc[i].parametric, &inv)) { + return false; + } + + float err = 0; + for (int j = 0; j < 3; ++j) { + err = fmaxf_(err, skcms_MaxRoundtripError(&profile->trc[j], &inv)); + } + if (min_max_error > err) { + min_max_error = err; + best_tf = i; + } + } + + for (int i = 0; i < 3; i++) { + result.trc[i].parametric = result.trc[best_tf].parametric; + } + + *profile = result; + assert_usable_as_destination(profile); + return true; +} diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/skcms.gni b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/skcms.gni new file mode 100644 index 00000000000..62fe24873d9 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/skcms.gni @@ -0,0 +1,57 @@ +# DO NOT EDIT: This is a generated file. +# See //bazel/exporter_tool/README.md for more information. +# +# The source of truth is //modules/skcms/BUILD.bazel + +# To update this file, run make -C bazel generate_gni + +_modules = get_path_info("../../modules", "abspath") + +# Generated by Bazel rule //modules/skcms:public_hdrs +skcms_public_headers = [ "$_modules/skcms/skcms.h" ] + +# List generated by Bazel rules: +# //modules/skcms:srcs +# //modules/skcms:textual_hdrs +skcms_sources = [ + "$_modules/skcms/skcms.cc", + "$_modules/skcms/src/Transform_inl.h", + "$_modules/skcms/src/skcms_Transform.h", + "$_modules/skcms/src/skcms_TransformBaseline.cc", + "$_modules/skcms/src/skcms_TransformHsw.cc", + "$_modules/skcms/src/skcms_TransformSkx.cc", + "$_modules/skcms/src/skcms_internals.h", + "$_modules/skcms/src/skcms_public.h", +] + +# Generated by Bazel rule //modules/skcms:skcms_public +skcms_public = [ + "$_modules/skcms/skcms.cc", + "$_modules/skcms/skcms.h", + "$_modules/skcms/src/skcms_internals.h", + "$_modules/skcms/src/skcms_public.h", +] + +# Generated by Bazel rule //modules/skcms:skcms_TransformBaseline +skcms_TransformBaseline = [ + "$_modules/skcms/src/skcms_Transform.h", + "$_modules/skcms/src/skcms_TransformBaseline.cc", + "$_modules/skcms/src/skcms_internals.h", + "$_modules/skcms/src/skcms_public.h", +] + +# Generated by Bazel rule //modules/skcms:skcms_TransformHsw +skcms_TransformHsw = [ + "$_modules/skcms/src/skcms_Transform.h", + "$_modules/skcms/src/skcms_TransformHsw.cc", + "$_modules/skcms/src/skcms_internals.h", + "$_modules/skcms/src/skcms_public.h", +] + +# Generated by Bazel rule //modules/skcms:skcms_TransformSkx +skcms_TransformSkx = [ + "$_modules/skcms/src/skcms_Transform.h", + "$_modules/skcms/src/skcms_TransformSkx.cc", + "$_modules/skcms/src/skcms_internals.h", + "$_modules/skcms/src/skcms_public.h", +] diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/skcms.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/skcms.h new file mode 100644 index 00000000000..7a9d4c1897b --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/skcms.h @@ -0,0 +1,10 @@ +/* + * Copyright 2023 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#pragma once + +#include "src/skcms_public.h" // NO_G3_REWRITE diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/Transform_inl.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/Transform_inl.h new file mode 100644 index 00000000000..b9c27ac3950 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/Transform_inl.h @@ -0,0 +1,1541 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +// Intentionally NO #pragma once... included multiple times. + +// This file is included from skcms.cc in a namespace with some pre-defines: +// - N: SIMD width of all vectors; 1, 4, 8 or 16 (preprocessor define) +// - V: a template to create a vector of N T's. + +using F = V; +using I32 = V; +using U64 = V; +using U32 = V; +using U16 = V; +using U8 = V; + +#if defined(__GNUC__) && !defined(__clang__) + // GCC is kind of weird, not allowing vector = scalar directly. + static constexpr F F0 = F() + 0.0f, + F1 = F() + 1.0f, + FInfBits = F() + 0x7f800000; // equals 2139095040, the bit pattern of +Inf +#else + static constexpr F F0 = 0.0f, + F1 = 1.0f, + FInfBits = 0x7f800000; // equals 2139095040, the bit pattern of +Inf +#endif + +// Instead of checking __AVX__ below, we'll check USING_AVX. +// This lets skcms.cc set USING_AVX to force us in even if the compiler's not set that way. +// Same deal for __F16C__ and __AVX2__ ~~~> USING_AVX_F16C, USING_AVX2. + +#if !defined(USING_AVX) && N == 8 && defined(__AVX__) + #define USING_AVX +#endif +#if !defined(USING_AVX_F16C) && defined(USING_AVX) && defined(__F16C__) + #define USING_AVX_F16C +#endif +#if !defined(USING_AVX2) && defined(USING_AVX) && defined(__AVX2__) + #define USING_AVX2 +#endif +#if !defined(USING_AVX512F) && N == 16 && defined(__AVX512F__) && defined(__AVX512DQ__) + #define USING_AVX512F +#endif + +// Similar to the AVX+ features, we define USING_NEON and USING_NEON_F16C. +// This is more for organizational clarity... skcms.cc doesn't force these. +#if N > 1 && defined(__ARM_NEON) + #define USING_NEON + + // We have to use two different mechanisms to enable the f16 conversion intrinsics: + #if defined(__clang__) + // Clang's arm_neon.h guards them with the FP hardware bit: + #if __ARM_FP & 2 + #define USING_NEON_F16C + #endif + #elif defined(__GNUC__) + // GCC's arm_neon.h guards them with the FP16 format macros (IEEE and ALTERNATIVE). + // We don't actually want the alternative format - we're reading/writing IEEE f16 values. + #if defined(__ARM_FP16_FORMAT_IEEE) + #define USING_NEON_F16C + #endif + #endif +#endif + +// These -Wvector-conversion warnings seem to trigger in very bogus situations, +// like vst3q_f32() expecting a 16x char rather than a 4x float vector. :/ +#if defined(USING_NEON) && defined(__clang__) + #pragma clang diagnostic ignored "-Wvector-conversion" +#endif + +// GCC & Clang (but not clang-cl) warn returning U64 on x86 is larger than a register. +// You'd see warnings like, "using AVX even though AVX is not enabled". +// We stifle these warnings; our helpers that return U64 are always inlined. +#if defined(__SSE__) && defined(__GNUC__) + #if !defined(__has_warning) + #pragma GCC diagnostic ignored "-Wpsabi" + #elif __has_warning("-Wpsabi") + #pragma GCC diagnostic ignored "-Wpsabi" + #endif +#endif + +// We tag most helper functions as SI, to enforce good code generation +// but also work around what we think is a bug in GCC: when targeting 32-bit +// x86, GCC tends to pass U16 (4x uint16_t vector) function arguments in the +// MMX mm0 register, which seems to mess with unrelated code that later uses +// x87 FP instructions (MMX's mm0 is an alias for x87's st0 register). +#if defined(__clang__) || defined(__GNUC__) + #define SI static inline __attribute__((always_inline)) +#else + #define SI static inline +#endif + +template +SI T load(const P* ptr) { + T val; + memcpy(&val, ptr, sizeof(val)); + return val; +} +template +SI void store(P* ptr, const T& val) { + memcpy(ptr, &val, sizeof(val)); +} + +// (T)v is a cast when N == 1 and a bit-pun when N>1, +// so we use cast(v) to actually cast or bit_pun(v) to bit-pun. +template +SI D cast(const S& v) { +#if N == 1 + return (D)v; +#elif defined(__clang__) + return __builtin_convertvector(v, D); +#else + D d; + for (int i = 0; i < N; i++) { + d[i] = v[i]; + } + return d; +#endif +} + +template +SI D bit_pun(const S& v) { + static_assert(sizeof(D) == sizeof(v), ""); + return load(&v); +} + +// When we convert from float to fixed point, it's very common to want to round, +// and for some reason compilers generate better code when converting to int32_t. +// To serve both those ends, we use this function to_fixed() instead of direct cast(). +SI U32 to_fixed(F f) { return (U32)cast(f + 0.5f); } + +// Sometimes we do something crazy on one branch of a conditonal, +// like divide by zero or convert a huge float to an integer, +// but then harmlessly select the other side. That trips up N==1 +// sanitizer builds, so we make if_then_else() a macro to avoid +// evaluating the unused side. + +#if N == 1 + #define if_then_else(cond, t, e) ((cond) ? (t) : (e)) +#else + template + SI T if_then_else(C cond, T t, T e) { + return bit_pun( ( cond & bit_pun(t)) | + (~cond & bit_pun(e)) ); + } +#endif + + +SI F F_from_Half(U16 half) { +#if defined(USING_NEON_F16C) + return vcvt_f32_f16((float16x4_t)half); +#elif defined(USING_AVX512F) + return (F)_mm512_cvtph_ps((__m256i)half); +#elif defined(USING_AVX_F16C) + typedef int16_t __attribute__((vector_size(16))) I16; + return __builtin_ia32_vcvtph2ps256((I16)half); +#else + U32 wide = cast(half); + // A half is 1-5-10 sign-exponent-mantissa, with 15 exponent bias. + U32 s = wide & 0x8000, + em = wide ^ s; + + // Constructing the float is easy if the half is not denormalized. + F norm = bit_pun( (s<<16) + (em<<13) + ((127-15)<<23) ); + + // Simply flush all denorm half floats to zero. + return if_then_else(em < 0x0400, F0, norm); +#endif +} + +#if defined(__clang__) + // The -((127-15)<<10) underflows that side of the math when + // we pass a denorm half float. It's harmless... we'll take the 0 side anyway. + __attribute__((no_sanitize("unsigned-integer-overflow"))) +#endif +SI U16 Half_from_F(F f) { +#if defined(USING_NEON_F16C) + return (U16)vcvt_f16_f32(f); +#elif defined(USING_AVX512F) + return (U16)_mm512_cvtps_ph((__m512 )f, _MM_FROUND_CUR_DIRECTION ); +#elif defined(USING_AVX_F16C) + return (U16)__builtin_ia32_vcvtps2ph256(f, 0x04/*_MM_FROUND_CUR_DIRECTION*/); +#else + // A float is 1-8-23 sign-exponent-mantissa, with 127 exponent bias. + U32 sem = bit_pun(f), + s = sem & 0x80000000, + em = sem ^ s; + + // For simplicity we flush denorm half floats (including all denorm floats) to zero. + return cast(if_then_else(em < 0x38800000, (U32)F0 + , (s>>16) + (em>>13) - ((127-15)<<10))); +#endif +} + +// Swap high and low bytes of 16-bit lanes, converting between big-endian and little-endian. +#if defined(USING_NEON) + SI U16 swap_endian_16(U16 v) { + return (U16)vrev16_u8((uint8x8_t) v); + } +#endif + +SI U64 swap_endian_16x4(const U64& rgba) { + return (rgba & 0x00ff00ff00ff00ff) << 8 + | (rgba & 0xff00ff00ff00ff00) >> 8; +} + +#if defined(USING_NEON) + SI F min_(F x, F y) { return (F)vminq_f32((float32x4_t)x, (float32x4_t)y); } + SI F max_(F x, F y) { return (F)vmaxq_f32((float32x4_t)x, (float32x4_t)y); } +#else + SI F min_(F x, F y) { return if_then_else(x > y, y, x); } + SI F max_(F x, F y) { return if_then_else(x < y, y, x); } +#endif + +SI F floor_(F x) { +#if N == 1 + return floorf_(x); +#elif defined(__aarch64__) + return vrndmq_f32(x); +#elif defined(USING_AVX512F) + // Clang's _mm512_floor_ps() passes its mask as -1, not (__mmask16)-1, + // and integer santizer catches that this implicit cast changes the + // value from -1 to 65535. We'll cast manually to work around it. + // Read this as `return _mm512_floor_ps(x)`. + return _mm512_mask_floor_ps(x, (__mmask16)-1, x); +#elif defined(USING_AVX) + return __builtin_ia32_roundps256(x, 0x01/*_MM_FROUND_FLOOR*/); +#elif defined(__SSE4_1__) + return _mm_floor_ps(x); +#else + // Round trip through integers with a truncating cast. + F roundtrip = cast(cast(x)); + // If x is negative, truncating gives the ceiling instead of the floor. + return roundtrip - if_then_else(roundtrip > x, F1, F0); + + // This implementation fails for values of x that are outside + // the range an integer can represent. We expect most x to be small. +#endif +} + +SI F approx_log2(F x) { + // The first approximation of log2(x) is its exponent 'e', minus 127. + I32 bits = bit_pun(x); + + F e = cast(bits) * (1.0f / (1<<23)); + + // If we use the mantissa too we can refine the error signficantly. + F m = bit_pun( (bits & 0x007fffff) | 0x3f000000 ); + + return e - 124.225514990f + - 1.498030302f*m + - 1.725879990f/(0.3520887068f + m); +} + +SI F approx_log(F x) { + const float ln2 = 0.69314718f; + return ln2 * approx_log2(x); +} + +SI F approx_exp2(F x) { + F fract = x - floor_(x); + + F fbits = (1.0f * (1<<23)) * (x + 121.274057500f + - 1.490129070f*fract + + 27.728023300f/(4.84252568f - fract)); + I32 bits = cast(min_(max_(fbits, F0), FInfBits)); + + return bit_pun(bits); +} + +SI F approx_pow(F x, float y) { + return if_then_else((x == F0) | (x == F1), x + , approx_exp2(approx_log2(x) * y)); +} + +SI F approx_exp(F x) { + const float log2_e = 1.4426950408889634074f; + return approx_exp2(log2_e * x); +} + +SI F strip_sign(F x, U32* sign) { + U32 bits = bit_pun(x); + *sign = bits & 0x80000000; + return bit_pun(bits ^ *sign); +} + +SI F apply_sign(F x, U32 sign) { + return bit_pun(sign | bit_pun(x)); +} + +// Return tf(x). +SI F apply_tf(const skcms_TransferFunction* tf, F x) { + // Peel off the sign bit and set x = |x|. + U32 sign; + x = strip_sign(x, &sign); + + // The transfer function has a linear part up to d, exponential at d and after. + F v = if_then_else(x < tf->d, tf->c*x + tf->f + , approx_pow(tf->a*x + tf->b, tf->g) + tf->e); + + // Tack the sign bit back on. + return apply_sign(v, sign); +} + +// Return the gamma function (|x|^G with the original sign re-applied to x). +SI F apply_gamma(const skcms_TransferFunction* tf, F x) { + U32 sign; + x = strip_sign(x, &sign); + return apply_sign(approx_pow(x, tf->g), sign); +} + +SI F apply_pq(const skcms_TransferFunction* tf, F x) { + U32 bits = bit_pun(x), + sign = bits & 0x80000000; + x = bit_pun(bits ^ sign); + + F v = approx_pow(max_(tf->a + tf->b * approx_pow(x, tf->c), F0) + / (tf->d + tf->e * approx_pow(x, tf->c)), + tf->f); + + return bit_pun(sign | bit_pun(v)); +} + +SI F apply_hlg(const skcms_TransferFunction* tf, F x) { + const float R = tf->a, G = tf->b, + a = tf->c, b = tf->d, c = tf->e, + K = tf->f + 1; + U32 bits = bit_pun(x), + sign = bits & 0x80000000; + x = bit_pun(bits ^ sign); + + F v = if_then_else(x*R <= 1, approx_pow(x*R, G) + , approx_exp((x-c)*a) + b); + + return K*bit_pun(sign | bit_pun(v)); +} + +SI F apply_hlginv(const skcms_TransferFunction* tf, F x) { + const float R = tf->a, G = tf->b, + a = tf->c, b = tf->d, c = tf->e, + K = tf->f + 1; + U32 bits = bit_pun(x), + sign = bits & 0x80000000; + x = bit_pun(bits ^ sign); + x /= K; + + F v = if_then_else(x <= 1, R * approx_pow(x, G) + , a * approx_log(x - b) + c); + + return bit_pun(sign | bit_pun(v)); +} + + +// Strided loads and stores of N values, starting from p. +template +SI T load_3(const P* p) { +#if N == 1 + return (T)p[0]; +#elif N == 4 + return T{p[ 0],p[ 3],p[ 6],p[ 9]}; +#elif N == 8 + return T{p[ 0],p[ 3],p[ 6],p[ 9], p[12],p[15],p[18],p[21]}; +#elif N == 16 + return T{p[ 0],p[ 3],p[ 6],p[ 9], p[12],p[15],p[18],p[21], + p[24],p[27],p[30],p[33], p[36],p[39],p[42],p[45]}; +#endif +} + +template +SI T load_4(const P* p) { +#if N == 1 + return (T)p[0]; +#elif N == 4 + return T{p[ 0],p[ 4],p[ 8],p[12]}; +#elif N == 8 + return T{p[ 0],p[ 4],p[ 8],p[12], p[16],p[20],p[24],p[28]}; +#elif N == 16 + return T{p[ 0],p[ 4],p[ 8],p[12], p[16],p[20],p[24],p[28], + p[32],p[36],p[40],p[44], p[48],p[52],p[56],p[60]}; +#endif +} + +template +SI void store_3(P* p, const T& v) { +#if N == 1 + p[0] = v; +#elif N == 4 + p[ 0] = v[ 0]; p[ 3] = v[ 1]; p[ 6] = v[ 2]; p[ 9] = v[ 3]; +#elif N == 8 + p[ 0] = v[ 0]; p[ 3] = v[ 1]; p[ 6] = v[ 2]; p[ 9] = v[ 3]; + p[12] = v[ 4]; p[15] = v[ 5]; p[18] = v[ 6]; p[21] = v[ 7]; +#elif N == 16 + p[ 0] = v[ 0]; p[ 3] = v[ 1]; p[ 6] = v[ 2]; p[ 9] = v[ 3]; + p[12] = v[ 4]; p[15] = v[ 5]; p[18] = v[ 6]; p[21] = v[ 7]; + p[24] = v[ 8]; p[27] = v[ 9]; p[30] = v[10]; p[33] = v[11]; + p[36] = v[12]; p[39] = v[13]; p[42] = v[14]; p[45] = v[15]; +#endif +} + +template +SI void store_4(P* p, const T& v) { +#if N == 1 + p[0] = v; +#elif N == 4 + p[ 0] = v[ 0]; p[ 4] = v[ 1]; p[ 8] = v[ 2]; p[12] = v[ 3]; +#elif N == 8 + p[ 0] = v[ 0]; p[ 4] = v[ 1]; p[ 8] = v[ 2]; p[12] = v[ 3]; + p[16] = v[ 4]; p[20] = v[ 5]; p[24] = v[ 6]; p[28] = v[ 7]; +#elif N == 16 + p[ 0] = v[ 0]; p[ 4] = v[ 1]; p[ 8] = v[ 2]; p[12] = v[ 3]; + p[16] = v[ 4]; p[20] = v[ 5]; p[24] = v[ 6]; p[28] = v[ 7]; + p[32] = v[ 8]; p[36] = v[ 9]; p[40] = v[10]; p[44] = v[11]; + p[48] = v[12]; p[52] = v[13]; p[56] = v[14]; p[60] = v[15]; +#endif +} + + +SI U8 gather_8(const uint8_t* p, I32 ix) { +#if N == 1 + U8 v = p[ix]; +#elif N == 4 + U8 v = { p[ix[0]], p[ix[1]], p[ix[2]], p[ix[3]] }; +#elif N == 8 + U8 v = { p[ix[0]], p[ix[1]], p[ix[2]], p[ix[3]], + p[ix[4]], p[ix[5]], p[ix[6]], p[ix[7]] }; +#elif N == 16 + U8 v = { p[ix[ 0]], p[ix[ 1]], p[ix[ 2]], p[ix[ 3]], + p[ix[ 4]], p[ix[ 5]], p[ix[ 6]], p[ix[ 7]], + p[ix[ 8]], p[ix[ 9]], p[ix[10]], p[ix[11]], + p[ix[12]], p[ix[13]], p[ix[14]], p[ix[15]] }; +#endif + return v; +} + +SI U16 gather_16(const uint8_t* p, I32 ix) { + // Load the i'th 16-bit value from p. + auto load_16 = [p](int i) { + return load(p + 2*i); + }; +#if N == 1 + U16 v = load_16(ix); +#elif N == 4 + U16 v = { load_16(ix[0]), load_16(ix[1]), load_16(ix[2]), load_16(ix[3]) }; +#elif N == 8 + U16 v = { load_16(ix[0]), load_16(ix[1]), load_16(ix[2]), load_16(ix[3]), + load_16(ix[4]), load_16(ix[5]), load_16(ix[6]), load_16(ix[7]) }; +#elif N == 16 + U16 v = { load_16(ix[ 0]), load_16(ix[ 1]), load_16(ix[ 2]), load_16(ix[ 3]), + load_16(ix[ 4]), load_16(ix[ 5]), load_16(ix[ 6]), load_16(ix[ 7]), + load_16(ix[ 8]), load_16(ix[ 9]), load_16(ix[10]), load_16(ix[11]), + load_16(ix[12]), load_16(ix[13]), load_16(ix[14]), load_16(ix[15]) }; +#endif + return v; +} + +SI U32 gather_32(const uint8_t* p, I32 ix) { + // Load the i'th 32-bit value from p. + auto load_32 = [p](int i) { + return load(p + 4*i); + }; +#if N == 1 + U32 v = load_32(ix); +#elif N == 4 + U32 v = { load_32(ix[0]), load_32(ix[1]), load_32(ix[2]), load_32(ix[3]) }; +#elif N == 8 + U32 v = { load_32(ix[0]), load_32(ix[1]), load_32(ix[2]), load_32(ix[3]), + load_32(ix[4]), load_32(ix[5]), load_32(ix[6]), load_32(ix[7]) }; +#elif N == 16 + U32 v = { load_32(ix[ 0]), load_32(ix[ 1]), load_32(ix[ 2]), load_32(ix[ 3]), + load_32(ix[ 4]), load_32(ix[ 5]), load_32(ix[ 6]), load_32(ix[ 7]), + load_32(ix[ 8]), load_32(ix[ 9]), load_32(ix[10]), load_32(ix[11]), + load_32(ix[12]), load_32(ix[13]), load_32(ix[14]), load_32(ix[15]) }; +#endif + // TODO: AVX2 and AVX-512 gathers (c.f. gather_24). + return v; +} + +SI U32 gather_24(const uint8_t* p, I32 ix) { + // First, back up a byte. Any place we're gathering from has a safe junk byte to read + // in front of it, either a previous table value, or some tag metadata. + p -= 1; + + // Load the i'th 24-bit value from p, and 1 extra byte. + auto load_24_32 = [p](int i) { + return load(p + 3*i); + }; + + // Now load multiples of 4 bytes (a junk byte, then r,g,b). +#if N == 1 + U32 v = load_24_32(ix); +#elif N == 4 + U32 v = { load_24_32(ix[0]), load_24_32(ix[1]), load_24_32(ix[2]), load_24_32(ix[3]) }; +#elif N == 8 && !defined(USING_AVX2) + U32 v = { load_24_32(ix[0]), load_24_32(ix[1]), load_24_32(ix[2]), load_24_32(ix[3]), + load_24_32(ix[4]), load_24_32(ix[5]), load_24_32(ix[6]), load_24_32(ix[7]) }; +#elif N == 8 + (void)load_24_32; + // The gather instruction here doesn't need any particular alignment, + // but the intrinsic takes a const int*. + const int* p4 = bit_pun(p); + I32 zero = { 0, 0, 0, 0, 0, 0, 0, 0}, + mask = {-1,-1,-1,-1, -1,-1,-1,-1}; + #if defined(__clang__) + U32 v = (U32)__builtin_ia32_gatherd_d256(zero, p4, 3*ix, mask, 1); + #elif defined(__GNUC__) + U32 v = (U32)__builtin_ia32_gathersiv8si(zero, p4, 3*ix, mask, 1); + #endif +#elif N == 16 + (void)load_24_32; + // The intrinsic is supposed to take const void* now, but it takes const int*, just like AVX2. + // And AVX-512 swapped the order of arguments. :/ + const int* p4 = bit_pun(p); + U32 v = (U32)_mm512_i32gather_epi32((__m512i)(3*ix), p4, 1); +#endif + + // Shift off the junk byte, leaving r,g,b in low 24 bits (and zero in the top 8). + return v >> 8; +} + +#if !defined(__arm__) + SI void gather_48(const uint8_t* p, I32 ix, U64* v) { + // As in gather_24(), with everything doubled. + p -= 2; + + // Load the i'th 48-bit value from p, and 2 extra bytes. + auto load_48_64 = [p](int i) { + return load(p + 6*i); + }; + + #if N == 1 + *v = load_48_64(ix); + #elif N == 4 + *v = U64{ + load_48_64(ix[0]), load_48_64(ix[1]), load_48_64(ix[2]), load_48_64(ix[3]), + }; + #elif N == 8 && !defined(USING_AVX2) + *v = U64{ + load_48_64(ix[0]), load_48_64(ix[1]), load_48_64(ix[2]), load_48_64(ix[3]), + load_48_64(ix[4]), load_48_64(ix[5]), load_48_64(ix[6]), load_48_64(ix[7]), + }; + #elif N == 8 + (void)load_48_64; + typedef int32_t __attribute__((vector_size(16))) Half_I32; + typedef long long __attribute__((vector_size(32))) Half_I64; + + // The gather instruction here doesn't need any particular alignment, + // but the intrinsic takes a const long long*. + const long long int* p8 = bit_pun(p); + + Half_I64 zero = { 0, 0, 0, 0}, + mask = {-1,-1,-1,-1}; + + ix *= 6; + Half_I32 ix_lo = { ix[0], ix[1], ix[2], ix[3] }, + ix_hi = { ix[4], ix[5], ix[6], ix[7] }; + + #if defined(__clang__) + Half_I64 lo = (Half_I64)__builtin_ia32_gatherd_q256(zero, p8, ix_lo, mask, 1), + hi = (Half_I64)__builtin_ia32_gatherd_q256(zero, p8, ix_hi, mask, 1); + #elif defined(__GNUC__) + Half_I64 lo = (Half_I64)__builtin_ia32_gathersiv4di(zero, p8, ix_lo, mask, 1), + hi = (Half_I64)__builtin_ia32_gathersiv4di(zero, p8, ix_hi, mask, 1); + #endif + store((char*)v + 0, lo); + store((char*)v + 32, hi); + #elif N == 16 + (void)load_48_64; + const long long int* p8 = bit_pun(p); + __m512i lo = _mm512_i32gather_epi64(_mm512_extracti32x8_epi32((__m512i)(6*ix), 0), p8, 1), + hi = _mm512_i32gather_epi64(_mm512_extracti32x8_epi32((__m512i)(6*ix), 1), p8, 1); + store((char*)v + 0, lo); + store((char*)v + 64, hi); + #endif + + *v >>= 16; + } +#endif + +SI F F_from_U8(U8 v) { + return cast(v) * (1/255.0f); +} + +SI F F_from_U16_BE(U16 v) { + // All 16-bit ICC values are big-endian, so we byte swap before converting to float. + // MSVC catches the "loss" of data here in the portable path, so we also make sure to mask. + U16 lo = (v >> 8), + hi = (v << 8) & 0xffff; + return cast(lo|hi) * (1/65535.0f); +} + +SI U16 U16_from_F(F v) { + // 65535 == inf in FP16, so promote to FP32 before converting. + return cast(cast>(v) * 65535 + 0.5f); +} + +SI F minus_1_ulp(F v) { + return bit_pun( bit_pun(v) - 1 ); +} + +SI F table(const skcms_Curve* curve, F v) { + // Clamp the input to [0,1], then scale to a table index. + F ix = max_(F0, min_(v, F1)) * (float)(curve->table_entries - 1); + + // We'll look up (equal or adjacent) entries at lo and hi, then lerp by t between the two. + I32 lo = cast( ix ), + hi = cast(minus_1_ulp(ix+1.0f)); + F t = ix - cast(lo); // i.e. the fractional part of ix. + + // TODO: can we load l and h simultaneously? Each entry in 'h' is either + // the same as in 'l' or adjacent. We have a rough idea that's it'd always be safe + // to read adjacent entries and perhaps underflow the table by a byte or two + // (it'd be junk, but always safe to read). Not sure how to lerp yet. + F l,h; + if (curve->table_8) { + l = F_from_U8(gather_8(curve->table_8, lo)); + h = F_from_U8(gather_8(curve->table_8, hi)); + } else { + l = F_from_U16_BE(gather_16(curve->table_16, lo)); + h = F_from_U16_BE(gather_16(curve->table_16, hi)); + } + return l + (h-l)*t; +} + +SI void sample_clut_8(const uint8_t* grid_8, I32 ix, F* r, F* g, F* b) { + U32 rgb = gather_24(grid_8, ix); + + *r = cast((rgb >> 0) & 0xff) * (1/255.0f); + *g = cast((rgb >> 8) & 0xff) * (1/255.0f); + *b = cast((rgb >> 16) & 0xff) * (1/255.0f); +} + +SI void sample_clut_8(const uint8_t* grid_8, I32 ix, F* r, F* g, F* b, F* a) { + // TODO: don't forget to optimize gather_32(). + U32 rgba = gather_32(grid_8, ix); + + *r = cast((rgba >> 0) & 0xff) * (1/255.0f); + *g = cast((rgba >> 8) & 0xff) * (1/255.0f); + *b = cast((rgba >> 16) & 0xff) * (1/255.0f); + *a = cast((rgba >> 24) & 0xff) * (1/255.0f); +} + +SI void sample_clut_16(const uint8_t* grid_16, I32 ix, F* r, F* g, F* b) { +#if defined(__arm__) + // This is up to 2x faster on 32-bit ARM than the #else-case fast path. + *r = F_from_U16_BE(gather_16(grid_16, 3*ix+0)); + *g = F_from_U16_BE(gather_16(grid_16, 3*ix+1)); + *b = F_from_U16_BE(gather_16(grid_16, 3*ix+2)); +#else + // This strategy is much faster for 64-bit builds, and fine for 32-bit x86 too. + U64 rgb; + gather_48(grid_16, ix, &rgb); + rgb = swap_endian_16x4(rgb); + + *r = cast((rgb >> 0) & 0xffff) * (1/65535.0f); + *g = cast((rgb >> 16) & 0xffff) * (1/65535.0f); + *b = cast((rgb >> 32) & 0xffff) * (1/65535.0f); +#endif +} + +SI void sample_clut_16(const uint8_t* grid_16, I32 ix, F* r, F* g, F* b, F* a) { + // TODO: gather_64()-based fast path? + *r = F_from_U16_BE(gather_16(grid_16, 4*ix+0)); + *g = F_from_U16_BE(gather_16(grid_16, 4*ix+1)); + *b = F_from_U16_BE(gather_16(grid_16, 4*ix+2)); + *a = F_from_U16_BE(gather_16(grid_16, 4*ix+3)); +} + +static void clut(uint32_t input_channels, uint32_t output_channels, + const uint8_t grid_points[4], const uint8_t* grid_8, const uint8_t* grid_16, + F* r, F* g, F* b, F* a) { + + const int dim = (int)input_channels; + assert (0 < dim && dim <= 4); + assert (output_channels == 3 || + output_channels == 4); + + // For each of these arrays, think foo[2*dim], but we use foo[8] since we know dim <= 4. + I32 index [8]; // Index contribution by dimension, first low from 0, then high from 4. + F weight[8]; // Weight for each contribution, again first low, then high. + + // O(dim) work first: calculate index,weight from r,g,b,a. + const F inputs[] = { *r,*g,*b,*a }; + for (int i = dim-1, stride = 1; i >= 0; i--) { + // x is where we logically want to sample the grid in the i-th dimension. + F x = inputs[i] * (float)(grid_points[i] - 1); + + // But we can't index at floats. lo and hi are the two integer grid points surrounding x. + I32 lo = cast( x ), // i.e. trunc(x) == floor(x) here. + hi = cast(minus_1_ulp(x+1.0f)); + // Notice how we fold in the accumulated stride across previous dimensions here. + index[i+0] = lo * stride; + index[i+4] = hi * stride; + stride *= grid_points[i]; + + // We'll interpolate between those two integer grid points by t. + F t = x - cast(lo); // i.e. fract(x) + weight[i+0] = 1-t; + weight[i+4] = t; + } + + *r = *g = *b = F0; + if (output_channels == 4) { + *a = F0; + } + + // We'll sample 2^dim == 1<input_channels, a2b->output_channels, + a2b->grid_points, a2b->grid_8, a2b->grid_16, + r,g,b,&a); +} +static void clut(const skcms_B2A* b2a, F* r, F* g, F* b, F* a) { + clut(b2a->input_channels, b2a->output_channels, + b2a->grid_points, b2a->grid_8, b2a->grid_16, + r,g,b,a); +} + +struct NoCtx {}; + +struct Ctx { + const void* fArg; + operator NoCtx() { return NoCtx{}; } + template operator T*() { return (const T*)fArg; } +}; + +#define STAGE_PARAMS(MAYBE_REF) SKCMS_MAYBE_UNUSED const char* src, \ + SKCMS_MAYBE_UNUSED char* dst, \ + SKCMS_MAYBE_UNUSED F MAYBE_REF r, \ + SKCMS_MAYBE_UNUSED F MAYBE_REF g, \ + SKCMS_MAYBE_UNUSED F MAYBE_REF b, \ + SKCMS_MAYBE_UNUSED F MAYBE_REF a, \ + SKCMS_MAYBE_UNUSED int i + +#if SKCMS_HAS_MUSTTAIL + + // Stages take a stage list, and each stage is responsible for tail-calling the next one. + // + // Unfortunately, we can't declare a StageFn as a function pointer which takes a pointer to + // another StageFn; declaring this leads to a circular dependency. To avoid this, StageFn is + // wrapped in a single-element `struct StageList` which we are able to forward-declare. + struct StageList; + using StageFn = void (*)(StageList stages, const void** ctx, STAGE_PARAMS()); + struct StageList { + const StageFn* fn; + }; + + #define DECLARE_STAGE(name, arg, CALL_NEXT) \ + SI void Exec_##name##_k(arg, STAGE_PARAMS(&)); \ + \ + SI void Exec_##name(StageList list, const void** ctx, STAGE_PARAMS()) { \ + Exec_##name##_k(Ctx{*ctx}, src, dst, r, g, b, a, i); \ + ++list.fn; ++ctx; \ + CALL_NEXT; \ + } \ + \ + SI void Exec_##name##_k(arg, STAGE_PARAMS(&)) + + #define STAGE(name, arg) \ + DECLARE_STAGE(name, arg, [[clang::musttail]] return (*list.fn)(list, ctx, src, dst, \ + r, g, b, a, i)) + + #define FINAL_STAGE(name, arg) \ + DECLARE_STAGE(name, arg, /* Stop executing stages and return to the caller. */) + +#else + + #define DECLARE_STAGE(name, arg) \ + SI void Exec_##name##_k(arg, STAGE_PARAMS(&)); \ + \ + SI void Exec_##name(const void* ctx, STAGE_PARAMS(&)) { \ + Exec_##name##_k(Ctx{ctx}, src, dst, r, g, b, a, i); \ + } \ + \ + SI void Exec_##name##_k(arg, STAGE_PARAMS(&)) + + #define STAGE(name, arg) DECLARE_STAGE(name, arg) + #define FINAL_STAGE(name, arg) DECLARE_STAGE(name, arg) + +#endif + +STAGE(load_a8, NoCtx) { + a = F_from_U8(load(src + 1*i)); +} + +STAGE(load_g8, NoCtx) { + r = g = b = F_from_U8(load(src + 1*i)); +} + +STAGE(load_4444, NoCtx) { + U16 abgr = load(src + 2*i); + + r = cast((abgr >> 12) & 0xf) * (1/15.0f); + g = cast((abgr >> 8) & 0xf) * (1/15.0f); + b = cast((abgr >> 4) & 0xf) * (1/15.0f); + a = cast((abgr >> 0) & 0xf) * (1/15.0f); +} + +STAGE(load_565, NoCtx) { + U16 rgb = load(src + 2*i); + + r = cast(rgb & (uint16_t)(31<< 0)) * (1.0f / (31<< 0)); + g = cast(rgb & (uint16_t)(63<< 5)) * (1.0f / (63<< 5)); + b = cast(rgb & (uint16_t)(31<<11)) * (1.0f / (31<<11)); +} + +STAGE(load_888, NoCtx) { + const uint8_t* rgb = (const uint8_t*)(src + 3*i); +#if defined(USING_NEON) + // There's no uint8x4x3_t or vld3 load for it, so we'll load each rgb pixel one at + // a time. Since we're doing that, we might as well load them into 16-bit lanes. + // (We'd even load into 32-bit lanes, but that's not possible on ARMv7.) + uint8x8x3_t v = {{ vdup_n_u8(0), vdup_n_u8(0), vdup_n_u8(0) }}; + v = vld3_lane_u8(rgb+0, v, 0); + v = vld3_lane_u8(rgb+3, v, 2); + v = vld3_lane_u8(rgb+6, v, 4); + v = vld3_lane_u8(rgb+9, v, 6); + + // Now if we squint, those 3 uint8x8_t we constructed are really U16s, easy to + // convert to F. (Again, U32 would be even better here if drop ARMv7 or split + // ARMv7 and ARMv8 impls.) + r = cast((U16)v.val[0]) * (1/255.0f); + g = cast((U16)v.val[1]) * (1/255.0f); + b = cast((U16)v.val[2]) * (1/255.0f); +#else + r = cast(load_3(rgb+0) ) * (1/255.0f); + g = cast(load_3(rgb+1) ) * (1/255.0f); + b = cast(load_3(rgb+2) ) * (1/255.0f); +#endif +} + +STAGE(load_8888, NoCtx) { + U32 rgba = load(src + 4*i); + + r = cast((rgba >> 0) & 0xff) * (1/255.0f); + g = cast((rgba >> 8) & 0xff) * (1/255.0f); + b = cast((rgba >> 16) & 0xff) * (1/255.0f); + a = cast((rgba >> 24) & 0xff) * (1/255.0f); +} + +STAGE(load_1010102, NoCtx) { + U32 rgba = load(src + 4*i); + + r = cast((rgba >> 0) & 0x3ff) * (1/1023.0f); + g = cast((rgba >> 10) & 0x3ff) * (1/1023.0f); + b = cast((rgba >> 20) & 0x3ff) * (1/1023.0f); + a = cast((rgba >> 30) & 0x3 ) * (1/ 3.0f); +} + +STAGE(load_101010x_XR, NoCtx) { + static constexpr float min = -0.752941f; + static constexpr float max = 1.25098f; + static constexpr float range = max - min; + U32 rgba = load(src + 4*i); + r = cast((rgba >> 0) & 0x3ff) * (1/1023.0f) * range + min; + g = cast((rgba >> 10) & 0x3ff) * (1/1023.0f) * range + min; + b = cast((rgba >> 20) & 0x3ff) * (1/1023.0f) * range + min; +} + +STAGE(load_10101010_XR, NoCtx) { + static constexpr float min = -0.752941f; + static constexpr float max = 1.25098f; + static constexpr float range = max - min; + U64 rgba = load(src + 8*i); + r = cast((rgba >> 0) & 0x3ff) * (1/1023.0f) * range + min; + g = cast((rgba >> 16) & 0x3ff) * (1/1023.0f) * range + min; + b = cast((rgba >> 32) & 0x3ff) * (1/1023.0f) * range + min; + a = cast((rgba >> 48) & 0x3ff) * (1/1023.0f) * range + min; +} + +STAGE(load_161616LE, NoCtx) { + uintptr_t ptr = (uintptr_t)(src + 6*i); + assert( (ptr & 1) == 0 ); // src must be 2-byte aligned for this + const uint16_t* rgb = (const uint16_t*)ptr; // cast to const uint16_t* to be safe. +#if defined(USING_NEON) + uint16x4x3_t v = vld3_u16(rgb); + r = cast((U16)v.val[0]) * (1/65535.0f); + g = cast((U16)v.val[1]) * (1/65535.0f); + b = cast((U16)v.val[2]) * (1/65535.0f); +#else + r = cast(load_3(rgb+0)) * (1/65535.0f); + g = cast(load_3(rgb+1)) * (1/65535.0f); + b = cast(load_3(rgb+2)) * (1/65535.0f); +#endif +} + +STAGE(load_16161616LE, NoCtx) { + uintptr_t ptr = (uintptr_t)(src + 8*i); + assert( (ptr & 1) == 0 ); // src must be 2-byte aligned for this + const uint16_t* rgba = (const uint16_t*)ptr; // cast to const uint16_t* to be safe. +#if defined(USING_NEON) + uint16x4x4_t v = vld4_u16(rgba); + r = cast((U16)v.val[0]) * (1/65535.0f); + g = cast((U16)v.val[1]) * (1/65535.0f); + b = cast((U16)v.val[2]) * (1/65535.0f); + a = cast((U16)v.val[3]) * (1/65535.0f); +#else + U64 px = load(rgba); + + r = cast((px >> 0) & 0xffff) * (1/65535.0f); + g = cast((px >> 16) & 0xffff) * (1/65535.0f); + b = cast((px >> 32) & 0xffff) * (1/65535.0f); + a = cast((px >> 48) & 0xffff) * (1/65535.0f); +#endif +} + +STAGE(load_161616BE, NoCtx) { + uintptr_t ptr = (uintptr_t)(src + 6*i); + assert( (ptr & 1) == 0 ); // src must be 2-byte aligned for this + const uint16_t* rgb = (const uint16_t*)ptr; // cast to const uint16_t* to be safe. +#if defined(USING_NEON) + uint16x4x3_t v = vld3_u16(rgb); + r = cast(swap_endian_16((U16)v.val[0])) * (1/65535.0f); + g = cast(swap_endian_16((U16)v.val[1])) * (1/65535.0f); + b = cast(swap_endian_16((U16)v.val[2])) * (1/65535.0f); +#else + U32 R = load_3(rgb+0), + G = load_3(rgb+1), + B = load_3(rgb+2); + // R,G,B are big-endian 16-bit, so byte swap them before converting to float. + r = cast((R & 0x00ff)<<8 | (R & 0xff00)>>8) * (1/65535.0f); + g = cast((G & 0x00ff)<<8 | (G & 0xff00)>>8) * (1/65535.0f); + b = cast((B & 0x00ff)<<8 | (B & 0xff00)>>8) * (1/65535.0f); +#endif +} + +STAGE(load_16161616BE, NoCtx) { + uintptr_t ptr = (uintptr_t)(src + 8*i); + assert( (ptr & 1) == 0 ); // src must be 2-byte aligned for this + const uint16_t* rgba = (const uint16_t*)ptr; // cast to const uint16_t* to be safe. +#if defined(USING_NEON) + uint16x4x4_t v = vld4_u16(rgba); + r = cast(swap_endian_16((U16)v.val[0])) * (1/65535.0f); + g = cast(swap_endian_16((U16)v.val[1])) * (1/65535.0f); + b = cast(swap_endian_16((U16)v.val[2])) * (1/65535.0f); + a = cast(swap_endian_16((U16)v.val[3])) * (1/65535.0f); +#else + U64 px = swap_endian_16x4(load(rgba)); + + r = cast((px >> 0) & 0xffff) * (1/65535.0f); + g = cast((px >> 16) & 0xffff) * (1/65535.0f); + b = cast((px >> 32) & 0xffff) * (1/65535.0f); + a = cast((px >> 48) & 0xffff) * (1/65535.0f); +#endif +} + +STAGE(load_hhh, NoCtx) { + uintptr_t ptr = (uintptr_t)(src + 6*i); + assert( (ptr & 1) == 0 ); // src must be 2-byte aligned for this + const uint16_t* rgb = (const uint16_t*)ptr; // cast to const uint16_t* to be safe. +#if defined(USING_NEON) + uint16x4x3_t v = vld3_u16(rgb); + U16 R = (U16)v.val[0], + G = (U16)v.val[1], + B = (U16)v.val[2]; +#else + U16 R = load_3(rgb+0), + G = load_3(rgb+1), + B = load_3(rgb+2); +#endif + r = F_from_Half(R); + g = F_from_Half(G); + b = F_from_Half(B); +} + +STAGE(load_hhhh, NoCtx) { + uintptr_t ptr = (uintptr_t)(src + 8*i); + assert( (ptr & 1) == 0 ); // src must be 2-byte aligned for this + const uint16_t* rgba = (const uint16_t*)ptr; // cast to const uint16_t* to be safe. +#if defined(USING_NEON) + uint16x4x4_t v = vld4_u16(rgba); + U16 R = (U16)v.val[0], + G = (U16)v.val[1], + B = (U16)v.val[2], + A = (U16)v.val[3]; +#else + U64 px = load(rgba); + U16 R = cast((px >> 0) & 0xffff), + G = cast((px >> 16) & 0xffff), + B = cast((px >> 32) & 0xffff), + A = cast((px >> 48) & 0xffff); +#endif + r = F_from_Half(R); + g = F_from_Half(G); + b = F_from_Half(B); + a = F_from_Half(A); +} + +STAGE(load_fff, NoCtx) { + uintptr_t ptr = (uintptr_t)(src + 12*i); + assert( (ptr & 3) == 0 ); // src must be 4-byte aligned for this + const float* rgb = (const float*)ptr; // cast to const float* to be safe. +#if defined(USING_NEON) + float32x4x3_t v = vld3q_f32(rgb); + r = (F)v.val[0]; + g = (F)v.val[1]; + b = (F)v.val[2]; +#else + r = load_3(rgb+0); + g = load_3(rgb+1); + b = load_3(rgb+2); +#endif +} + +STAGE(load_ffff, NoCtx) { + uintptr_t ptr = (uintptr_t)(src + 16*i); + assert( (ptr & 3) == 0 ); // src must be 4-byte aligned for this + const float* rgba = (const float*)ptr; // cast to const float* to be safe. +#if defined(USING_NEON) + float32x4x4_t v = vld4q_f32(rgba); + r = (F)v.val[0]; + g = (F)v.val[1]; + b = (F)v.val[2]; + a = (F)v.val[3]; +#else + r = load_4(rgba+0); + g = load_4(rgba+1); + b = load_4(rgba+2); + a = load_4(rgba+3); +#endif +} + +STAGE(swap_rb, NoCtx) { + F t = r; + r = b; + b = t; +} + +STAGE(clamp, NoCtx) { + r = max_(F0, min_(r, F1)); + g = max_(F0, min_(g, F1)); + b = max_(F0, min_(b, F1)); + a = max_(F0, min_(a, F1)); +} + +STAGE(invert, NoCtx) { + r = F1 - r; + g = F1 - g; + b = F1 - b; + a = F1 - a; +} + +STAGE(force_opaque, NoCtx) { + a = F1; +} + +STAGE(premul, NoCtx) { + r *= a; + g *= a; + b *= a; +} + +STAGE(unpremul, NoCtx) { + F scale = if_then_else(F1 / a < INFINITY_, F1 / a, F0); + r *= scale; + g *= scale; + b *= scale; +} + +STAGE(matrix_3x3, const skcms_Matrix3x3* matrix) { + const float* m = &matrix->vals[0][0]; + + F R = m[0]*r + m[1]*g + m[2]*b, + G = m[3]*r + m[4]*g + m[5]*b, + B = m[6]*r + m[7]*g + m[8]*b; + + r = R; + g = G; + b = B; +} + +STAGE(matrix_3x4, const skcms_Matrix3x4* matrix) { + const float* m = &matrix->vals[0][0]; + + F R = m[0]*r + m[1]*g + m[ 2]*b + m[ 3], + G = m[4]*r + m[5]*g + m[ 6]*b + m[ 7], + B = m[8]*r + m[9]*g + m[10]*b + m[11]; + + r = R; + g = G; + b = B; +} + +STAGE(lab_to_xyz, NoCtx) { + // The L*a*b values are in r,g,b, but normalized to [0,1]. Reconstruct them: + F L = r * 100.0f, + A = g * 255.0f - 128.0f, + B = b * 255.0f - 128.0f; + + // Convert to CIE XYZ. + F Y = (L + 16.0f) * (1/116.0f), + X = Y + A*(1/500.0f), + Z = Y - B*(1/200.0f); + + X = if_then_else(X*X*X > 0.008856f, X*X*X, (X - (16/116.0f)) * (1/7.787f)); + Y = if_then_else(Y*Y*Y > 0.008856f, Y*Y*Y, (Y - (16/116.0f)) * (1/7.787f)); + Z = if_then_else(Z*Z*Z > 0.008856f, Z*Z*Z, (Z - (16/116.0f)) * (1/7.787f)); + + // Adjust to XYZD50 illuminant, and stuff back into r,g,b for the next op. + r = X * 0.9642f; + g = Y ; + b = Z * 0.8249f; +} + +// As above, in reverse. +STAGE(xyz_to_lab, NoCtx) { + F X = r * (1/0.9642f), + Y = g, + Z = b * (1/0.8249f); + + X = if_then_else(X > 0.008856f, approx_pow(X, 1/3.0f), X*7.787f + (16/116.0f)); + Y = if_then_else(Y > 0.008856f, approx_pow(Y, 1/3.0f), Y*7.787f + (16/116.0f)); + Z = if_then_else(Z > 0.008856f, approx_pow(Z, 1/3.0f), Z*7.787f + (16/116.0f)); + + F L = Y*116.0f - 16.0f, + A = (X-Y)*500.0f, + B = (Y-Z)*200.0f; + + r = L * (1/100.f); + g = (A + 128.0f) * (1/255.0f); + b = (B + 128.0f) * (1/255.0f); +} + +STAGE(gamma_r, const skcms_TransferFunction* tf) { r = apply_gamma(tf, r); } +STAGE(gamma_g, const skcms_TransferFunction* tf) { g = apply_gamma(tf, g); } +STAGE(gamma_b, const skcms_TransferFunction* tf) { b = apply_gamma(tf, b); } +STAGE(gamma_a, const skcms_TransferFunction* tf) { a = apply_gamma(tf, a); } + +STAGE(gamma_rgb, const skcms_TransferFunction* tf) { + r = apply_gamma(tf, r); + g = apply_gamma(tf, g); + b = apply_gamma(tf, b); +} + +STAGE(tf_r, const skcms_TransferFunction* tf) { r = apply_tf(tf, r); } +STAGE(tf_g, const skcms_TransferFunction* tf) { g = apply_tf(tf, g); } +STAGE(tf_b, const skcms_TransferFunction* tf) { b = apply_tf(tf, b); } +STAGE(tf_a, const skcms_TransferFunction* tf) { a = apply_tf(tf, a); } + +STAGE(tf_rgb, const skcms_TransferFunction* tf) { + r = apply_tf(tf, r); + g = apply_tf(tf, g); + b = apply_tf(tf, b); +} + +STAGE(pq_r, const skcms_TransferFunction* tf) { r = apply_pq(tf, r); } +STAGE(pq_g, const skcms_TransferFunction* tf) { g = apply_pq(tf, g); } +STAGE(pq_b, const skcms_TransferFunction* tf) { b = apply_pq(tf, b); } +STAGE(pq_a, const skcms_TransferFunction* tf) { a = apply_pq(tf, a); } + +STAGE(pq_rgb, const skcms_TransferFunction* tf) { + r = apply_pq(tf, r); + g = apply_pq(tf, g); + b = apply_pq(tf, b); +} + +STAGE(hlg_r, const skcms_TransferFunction* tf) { r = apply_hlg(tf, r); } +STAGE(hlg_g, const skcms_TransferFunction* tf) { g = apply_hlg(tf, g); } +STAGE(hlg_b, const skcms_TransferFunction* tf) { b = apply_hlg(tf, b); } +STAGE(hlg_a, const skcms_TransferFunction* tf) { a = apply_hlg(tf, a); } + +STAGE(hlg_rgb, const skcms_TransferFunction* tf) { + r = apply_hlg(tf, r); + g = apply_hlg(tf, g); + b = apply_hlg(tf, b); +} + +STAGE(hlginv_r, const skcms_TransferFunction* tf) { r = apply_hlginv(tf, r); } +STAGE(hlginv_g, const skcms_TransferFunction* tf) { g = apply_hlginv(tf, g); } +STAGE(hlginv_b, const skcms_TransferFunction* tf) { b = apply_hlginv(tf, b); } +STAGE(hlginv_a, const skcms_TransferFunction* tf) { a = apply_hlginv(tf, a); } + +STAGE(hlginv_rgb, const skcms_TransferFunction* tf) { + r = apply_hlginv(tf, r); + g = apply_hlginv(tf, g); + b = apply_hlginv(tf, b); +} + +STAGE(table_r, const skcms_Curve* curve) { r = table(curve, r); } +STAGE(table_g, const skcms_Curve* curve) { g = table(curve, g); } +STAGE(table_b, const skcms_Curve* curve) { b = table(curve, b); } +STAGE(table_a, const skcms_Curve* curve) { a = table(curve, a); } + +STAGE(clut_A2B, const skcms_A2B* a2b) { + clut(a2b, &r,&g,&b,a); + + if (a2b->input_channels == 4) { + // CMYK is opaque. + a = F1; + } +} + +STAGE(clut_B2A, const skcms_B2A* b2a) { + clut(b2a, &r,&g,&b,&a); +} + +// From here on down, the store_ ops are all "final stages," terminating processing of this group. + +FINAL_STAGE(store_a8, NoCtx) { + store(dst + 1*i, cast(to_fixed(a * 255))); +} + +FINAL_STAGE(store_g8, NoCtx) { + // g should be holding luminance (Y) (r,g,b ~~~> X,Y,Z) + store(dst + 1*i, cast(to_fixed(g * 255))); +} + +FINAL_STAGE(store_4444, NoCtx) { + store(dst + 2*i, cast(to_fixed(r * 15) << 12) + | cast(to_fixed(g * 15) << 8) + | cast(to_fixed(b * 15) << 4) + | cast(to_fixed(a * 15) << 0)); +} + +FINAL_STAGE(store_565, NoCtx) { + store(dst + 2*i, cast(to_fixed(r * 31) << 0 ) + | cast(to_fixed(g * 63) << 5 ) + | cast(to_fixed(b * 31) << 11 )); +} + +FINAL_STAGE(store_888, NoCtx) { + uint8_t* rgb = (uint8_t*)dst + 3*i; +#if defined(USING_NEON) + // Same deal as load_888 but in reverse... we'll store using uint8x8x3_t, but + // get there via U16 to save some instructions converting to float. And just + // like load_888, we'd prefer to go via U32 but for ARMv7 support. + U16 R = cast(to_fixed(r * 255)), + G = cast(to_fixed(g * 255)), + B = cast(to_fixed(b * 255)); + + uint8x8x3_t v = {{ (uint8x8_t)R, (uint8x8_t)G, (uint8x8_t)B }}; + vst3_lane_u8(rgb+0, v, 0); + vst3_lane_u8(rgb+3, v, 2); + vst3_lane_u8(rgb+6, v, 4); + vst3_lane_u8(rgb+9, v, 6); +#else + store_3(rgb+0, cast(to_fixed(r * 255)) ); + store_3(rgb+1, cast(to_fixed(g * 255)) ); + store_3(rgb+2, cast(to_fixed(b * 255)) ); +#endif +} + +FINAL_STAGE(store_8888, NoCtx) { + store(dst + 4*i, cast(to_fixed(r * 255)) << 0 + | cast(to_fixed(g * 255)) << 8 + | cast(to_fixed(b * 255)) << 16 + | cast(to_fixed(a * 255)) << 24); +} + +FINAL_STAGE(store_101010x_XR, NoCtx) { + static constexpr float min = -0.752941f; + static constexpr float max = 1.25098f; + static constexpr float range = max - min; + store(dst + 4*i, cast(to_fixed(((r - min) / range) * 1023)) << 0 + | cast(to_fixed(((g - min) / range) * 1023)) << 10 + | cast(to_fixed(((b - min) / range) * 1023)) << 20); +} + +FINAL_STAGE(store_1010102, NoCtx) { + store(dst + 4*i, cast(to_fixed(r * 1023)) << 0 + | cast(to_fixed(g * 1023)) << 10 + | cast(to_fixed(b * 1023)) << 20 + | cast(to_fixed(a * 3)) << 30); +} + +FINAL_STAGE(store_161616LE, NoCtx) { + uintptr_t ptr = (uintptr_t)(dst + 6*i); + assert( (ptr & 1) == 0 ); // The dst pointer must be 2-byte aligned + uint16_t* rgb = (uint16_t*)ptr; // for this cast to uint16_t* to be safe. +#if defined(USING_NEON) + uint16x4x3_t v = {{ + (uint16x4_t)U16_from_F(r), + (uint16x4_t)U16_from_F(g), + (uint16x4_t)U16_from_F(b), + }}; + vst3_u16(rgb, v); +#else + store_3(rgb+0, U16_from_F(r)); + store_3(rgb+1, U16_from_F(g)); + store_3(rgb+2, U16_from_F(b)); +#endif + +} + +FINAL_STAGE(store_16161616LE, NoCtx) { + uintptr_t ptr = (uintptr_t)(dst + 8*i); + assert( (ptr & 1) == 0 ); // The dst pointer must be 2-byte aligned + uint16_t* rgba = (uint16_t*)ptr; // for this cast to uint16_t* to be safe. +#if defined(USING_NEON) + uint16x4x4_t v = {{ + (uint16x4_t)U16_from_F(r), + (uint16x4_t)U16_from_F(g), + (uint16x4_t)U16_from_F(b), + (uint16x4_t)U16_from_F(a), + }}; + vst4_u16(rgba, v); +#else + U64 px = cast(to_fixed(r * 65535)) << 0 + | cast(to_fixed(g * 65535)) << 16 + | cast(to_fixed(b * 65535)) << 32 + | cast(to_fixed(a * 65535)) << 48; + store(rgba, px); +#endif +} + +FINAL_STAGE(store_161616BE, NoCtx) { + uintptr_t ptr = (uintptr_t)(dst + 6*i); + assert( (ptr & 1) == 0 ); // The dst pointer must be 2-byte aligned + uint16_t* rgb = (uint16_t*)ptr; // for this cast to uint16_t* to be safe. +#if defined(USING_NEON) + uint16x4x3_t v = {{ + (uint16x4_t)swap_endian_16(cast(U16_from_F(r))), + (uint16x4_t)swap_endian_16(cast(U16_from_F(g))), + (uint16x4_t)swap_endian_16(cast(U16_from_F(b))), + }}; + vst3_u16(rgb, v); +#else + U32 R = to_fixed(r * 65535), + G = to_fixed(g * 65535), + B = to_fixed(b * 65535); + store_3(rgb+0, cast((R & 0x00ff) << 8 | (R & 0xff00) >> 8) ); + store_3(rgb+1, cast((G & 0x00ff) << 8 | (G & 0xff00) >> 8) ); + store_3(rgb+2, cast((B & 0x00ff) << 8 | (B & 0xff00) >> 8) ); +#endif + +} + +FINAL_STAGE(store_16161616BE, NoCtx) { + uintptr_t ptr = (uintptr_t)(dst + 8*i); + assert( (ptr & 1) == 0 ); // The dst pointer must be 2-byte aligned + uint16_t* rgba = (uint16_t*)ptr; // for this cast to uint16_t* to be safe. +#if defined(USING_NEON) + uint16x4x4_t v = {{ + (uint16x4_t)swap_endian_16(cast(U16_from_F(r))), + (uint16x4_t)swap_endian_16(cast(U16_from_F(g))), + (uint16x4_t)swap_endian_16(cast(U16_from_F(b))), + (uint16x4_t)swap_endian_16(cast(U16_from_F(a))), + }}; + vst4_u16(rgba, v); +#else + U64 px = cast(to_fixed(r * 65535)) << 0 + | cast(to_fixed(g * 65535)) << 16 + | cast(to_fixed(b * 65535)) << 32 + | cast(to_fixed(a * 65535)) << 48; + store(rgba, swap_endian_16x4(px)); +#endif +} + +FINAL_STAGE(store_hhh, NoCtx) { + uintptr_t ptr = (uintptr_t)(dst + 6*i); + assert( (ptr & 1) == 0 ); // The dst pointer must be 2-byte aligned + uint16_t* rgb = (uint16_t*)ptr; // for this cast to uint16_t* to be safe. + + U16 R = Half_from_F(r), + G = Half_from_F(g), + B = Half_from_F(b); +#if defined(USING_NEON) + uint16x4x3_t v = {{ + (uint16x4_t)R, + (uint16x4_t)G, + (uint16x4_t)B, + }}; + vst3_u16(rgb, v); +#else + store_3(rgb+0, R); + store_3(rgb+1, G); + store_3(rgb+2, B); +#endif +} + +FINAL_STAGE(store_hhhh, NoCtx) { + uintptr_t ptr = (uintptr_t)(dst + 8*i); + assert( (ptr & 1) == 0 ); // The dst pointer must be 2-byte aligned + uint16_t* rgba = (uint16_t*)ptr; // for this cast to uint16_t* to be safe. + + U16 R = Half_from_F(r), + G = Half_from_F(g), + B = Half_from_F(b), + A = Half_from_F(a); +#if defined(USING_NEON) + uint16x4x4_t v = {{ + (uint16x4_t)R, + (uint16x4_t)G, + (uint16x4_t)B, + (uint16x4_t)A, + }}; + vst4_u16(rgba, v); +#else + store(rgba, cast(R) << 0 + | cast(G) << 16 + | cast(B) << 32 + | cast(A) << 48); +#endif +} + +FINAL_STAGE(store_fff, NoCtx) { + uintptr_t ptr = (uintptr_t)(dst + 12*i); + assert( (ptr & 3) == 0 ); // The dst pointer must be 4-byte aligned + float* rgb = (float*)ptr; // for this cast to float* to be safe. +#if defined(USING_NEON) + float32x4x3_t v = {{ + (float32x4_t)r, + (float32x4_t)g, + (float32x4_t)b, + }}; + vst3q_f32(rgb, v); +#else + store_3(rgb+0, r); + store_3(rgb+1, g); + store_3(rgb+2, b); +#endif +} + +FINAL_STAGE(store_ffff, NoCtx) { + uintptr_t ptr = (uintptr_t)(dst + 16*i); + assert( (ptr & 3) == 0 ); // The dst pointer must be 4-byte aligned + float* rgba = (float*)ptr; // for this cast to float* to be safe. +#if defined(USING_NEON) + float32x4x4_t v = {{ + (float32x4_t)r, + (float32x4_t)g, + (float32x4_t)b, + (float32x4_t)a, + }}; + vst4q_f32(rgba, v); +#else + store_4(rgba+0, r); + store_4(rgba+1, g); + store_4(rgba+2, b); + store_4(rgba+3, a); +#endif +} + +#if SKCMS_HAS_MUSTTAIL + + SI void exec_stages(StageFn* stages, const void** contexts, const char* src, char* dst, int i) { + (*stages)({stages}, contexts, src, dst, F0, F0, F0, F1, i); + } + +#else + + static void exec_stages(const Op* ops, const void** contexts, + const char* src, char* dst, int i) { + F r = F0, g = F0, b = F0, a = F1; + while (true) { + switch (*ops++) { +#define M(name) case Op::name: Exec_##name(*contexts++, src, dst, r, g, b, a, i); break; + SKCMS_WORK_OPS(M) +#undef M +#define M(name) case Op::name: Exec_##name(*contexts++, src, dst, r, g, b, a, i); return; + SKCMS_STORE_OPS(M) +#undef M + } + } + } + +#endif + +// NOLINTNEXTLINE(misc-definitions-in-headers) +void run_program(const Op* program, const void** contexts, SKCMS_MAYBE_UNUSED ptrdiff_t programSize, + const char* src, char* dst, int n, + const size_t src_bpp, const size_t dst_bpp) { +#if SKCMS_HAS_MUSTTAIL + // Convert the program into an array of tailcall stages. + StageFn stages[32]; + assert(programSize <= ARRAY_COUNT(stages)); + + static constexpr StageFn kStageFns[] = { +#define M(name) &Exec_##name, + SKCMS_WORK_OPS(M) + SKCMS_STORE_OPS(M) +#undef M + }; + + for (ptrdiff_t index = 0; index < programSize; ++index) { + stages[index] = kStageFns[(int)program[index]]; + } +#else + // Use the op array as-is. + const Op* stages = program; +#endif + + int i = 0; + while (n >= N) { + exec_stages(stages, contexts, src, dst, i); + i += N; + n -= N; + } + if (n > 0) { + char tmp[4*4*N] = {0}; + + memcpy(tmp, (const char*)src + (size_t)i*src_bpp, (size_t)n*src_bpp); + exec_stages(stages, contexts, tmp, tmp, 0); + memcpy((char*)dst + (size_t)i*dst_bpp, tmp, (size_t)n*dst_bpp); + } +} diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_Transform.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_Transform.h new file mode 100644 index 00000000000..9f02e792fb4 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_Transform.h @@ -0,0 +1,162 @@ +/* + * Copyright 2018 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#pragma once + +#include +#include + +// skcms_Transform.h contains skcms implementation details. +// Please don't use this header from outside the skcms repo. + +namespace skcms_private { + +/** All transform ops */ + +#define SKCMS_WORK_OPS(M) \ + M(load_a8) \ + M(load_g8) \ + M(load_4444) \ + M(load_565) \ + M(load_888) \ + M(load_8888) \ + M(load_1010102) \ + M(load_101010x_XR) \ + M(load_10101010_XR) \ + M(load_161616LE) \ + M(load_16161616LE) \ + M(load_161616BE) \ + M(load_16161616BE) \ + M(load_hhh) \ + M(load_hhhh) \ + M(load_fff) \ + M(load_ffff) \ + \ + M(swap_rb) \ + M(clamp) \ + M(invert) \ + M(force_opaque) \ + M(premul) \ + M(unpremul) \ + M(matrix_3x3) \ + M(matrix_3x4) \ + \ + M(lab_to_xyz) \ + M(xyz_to_lab) \ + \ + M(gamma_r) \ + M(gamma_g) \ + M(gamma_b) \ + M(gamma_a) \ + M(gamma_rgb) \ + \ + M(tf_r) \ + M(tf_g) \ + M(tf_b) \ + M(tf_a) \ + M(tf_rgb) \ + \ + M(pq_r) \ + M(pq_g) \ + M(pq_b) \ + M(pq_a) \ + M(pq_rgb) \ + \ + M(hlg_r) \ + M(hlg_g) \ + M(hlg_b) \ + M(hlg_a) \ + M(hlg_rgb) \ + \ + M(hlginv_r) \ + M(hlginv_g) \ + M(hlginv_b) \ + M(hlginv_a) \ + M(hlginv_rgb) \ + \ + M(table_r) \ + M(table_g) \ + M(table_b) \ + M(table_a) \ + \ + M(clut_A2B) \ + M(clut_B2A) + +#define SKCMS_STORE_OPS(M) \ + M(store_a8) \ + M(store_g8) \ + M(store_4444) \ + M(store_565) \ + M(store_888) \ + M(store_8888) \ + M(store_1010102) \ + M(store_161616LE) \ + M(store_16161616LE) \ + M(store_161616BE) \ + M(store_16161616BE) \ + M(store_101010x_XR) \ + M(store_hhh) \ + M(store_hhhh) \ + M(store_fff) \ + M(store_ffff) + +enum class Op : int { +#define M(op) op, + SKCMS_WORK_OPS(M) + SKCMS_STORE_OPS(M) +#undef M +}; + +/** Constants */ + +#if defined(__clang__) || defined(__GNUC__) + static constexpr float INFINITY_ = __builtin_inff(); +#else + static const union { + uint32_t bits; + float f; + } inf_ = { 0x7f800000 }; + #define INFINITY_ inf_.f +#endif + +/** Vector type */ + +#if defined(__clang__) + template using Vec = T __attribute__((ext_vector_type(N))); +#elif defined(__GNUC__) + // Unfortunately, GCC does not allow us to omit the struct. This will not compile: + // template using Vec = T __attribute__((vector_size(N*sizeof(T)))); + template struct VecHelper { + typedef T __attribute__((vector_size(N * sizeof(T)))) V; + }; + template using Vec = typename VecHelper::V; +#endif + +/** Interface */ + +namespace baseline { + +void run_program(const Op* program, const void** contexts, ptrdiff_t programSize, + const char* src, char* dst, int n, + const size_t src_bpp, const size_t dst_bpp); + +} +namespace hsw { + +void run_program(const Op* program, const void** contexts, ptrdiff_t programSize, + const char* src, char* dst, int n, + const size_t src_bpp, const size_t dst_bpp); + +} +namespace skx { + +void run_program(const Op* program, const void** contexts, ptrdiff_t programSize, + const char* src, char* dst, int n, + const size_t src_bpp, const size_t dst_bpp); + +} +} // namespace skcms_private diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_TransformBaseline.cc b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_TransformBaseline.cc new file mode 100644 index 00000000000..bfe1df60a08 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_TransformBaseline.cc @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "skcms_public.h" // NO_G3_REWRITE +#include "skcms_internals.h" // NO_G3_REWRITE +#include "skcms_Transform.h" // NO_G3_REWRITE +#include +#include +#include +#include +#include + +#if defined(__ARM_NEON) + #include +#elif defined(__SSE__) + #include + + #if defined(__clang__) + // That #include is usually enough, but Clang's headers + // avoid #including the whole kitchen sink when _MSC_VER is defined, + // because lots of programs on Windows would include that and it'd be + // a lot slower. But we want all those headers included, so we can use + // their features (after making runtime checks). + #include + #endif +#endif + +namespace skcms_private { +namespace baseline { + +#if defined(SKCMS_PORTABLE) + // Build skcms in a portable scalar configuration. + #define N 1 + template using V = T; +#else + // Build skcms with basic four-line SIMD support. (SSE on Intel, or Neon on ARM) + #define N 4 + template using V = skcms_private::Vec; +#endif + +#include "Transform_inl.h" + +} // namespace baseline +} // namespace skcms_private diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_TransformHsw.cc b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_TransformHsw.cc new file mode 100644 index 00000000000..cd3673b1b0d --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_TransformHsw.cc @@ -0,0 +1,61 @@ +/* + * Copyright 2018 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "skcms_public.h" // NO_G3_REWRITE +#include "skcms_internals.h" // NO_G3_REWRITE +#include "skcms_Transform.h" // NO_G3_REWRITE +#include +#include +#include +#include +#include + +#if defined(__ARM_NEON) + #include +#elif defined(__SSE__) + #include + + #if defined(__clang__) + // That #include is usually enough, but Clang's headers + // avoid #including the whole kitchen sink when _MSC_VER is defined, + // because lots of programs on Windows would include that and it'd be + // a lot slower. But we want all those headers included, so we can use + // their features (after making runtime checks). + #include + #include + #include + #include + #include + #endif +#endif + +namespace skcms_private { +namespace hsw { + +#if defined(SKCMS_DISABLE_HSW) + +void run_program(const Op* program, const void** contexts, ptrdiff_t programSize, + const char* src, char* dst, int n, + const size_t src_bpp, const size_t dst_bpp) { + skcms_private::baseline::run_program(program, contexts, programSize, + src, dst, n, src_bpp, dst_bpp); +} + +#else + +#define USING_AVX +#define USING_AVX_F16C +#define USING_AVX2 +#define N 8 +template using V = skcms_private::Vec; + +#include "Transform_inl.h" + +#endif + +} // namespace hsw +} // namespace skcms_private diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_TransformSkx.cc b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_TransformSkx.cc new file mode 100644 index 00000000000..3e849dd4ecc --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_TransformSkx.cc @@ -0,0 +1,58 @@ +/* + * Copyright 2018 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "skcms_public.h" // NO_G3_REWRITE +#include "skcms_internals.h" // NO_G3_REWRITE +#include "skcms_Transform.h" // NO_G3_REWRITE +#include +#include +#include +#include +#include + +#if defined(__ARM_NEON) + #include +#elif defined(__SSE__) + #include + + #if defined(__clang__) + // That #include is usually enough, but Clang's headers + // avoid #including the whole kitchen sink when _MSC_VER is defined, + // because lots of programs on Windows would include that and it'd be + // a lot slower. But we want all those headers included, so we can use + // their features (after making runtime checks). + #include + #include + #include + #include + #include + #endif +#endif + +namespace skcms_private { +namespace skx { + +#if defined(SKCMS_DISABLE_SKX) + +void run_program(const Op* program, const void** contexts, ptrdiff_t programSize, + const char* src, char* dst, int n, + const size_t src_bpp, const size_t dst_bpp) { + skcms_private::baseline::run_program(program, contexts, programSize, + src, dst, n, src_bpp, dst_bpp); +} + +#else + +#define USING_AVX512F +#define N 16 +template using V = skcms_private::Vec; +#include "Transform_inl.h" + +#endif + +} // namespace skx +} // namespace skcms_private diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_internals.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_internals.h new file mode 100644 index 00000000000..f3f0a2d6cb5 --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_internals.h @@ -0,0 +1,138 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#pragma once + +// skcms_internals.h contains APIs shared by skcms' internals and its test tools. +// Please don't use this header from outside the skcms repo. + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// ~~~~ General Helper Macros ~~~~ +// skcms can leverage some C++ extensions when they are present. +#define ARRAY_COUNT(arr) (int)(sizeof((arr)) / sizeof(*(arr))) + +#if defined(__clang__) && defined(__has_cpp_attribute) + #if __has_cpp_attribute(clang::fallthrough) + #define SKCMS_FALLTHROUGH [[clang::fallthrough]] + #endif + + #ifndef SKCMS_HAS_MUSTTAIL + // [[clang::musttail]] is great for performance, but it's not well supported and we run into + // a variety of problems when we use it. Fortunately, it's an optional feature that doesn't + // affect correctness, and usually the compiler will generate a tail-call even for us + // whether or not we force it to do so. + // + // Known limitations: + // - Sanitizers do not work well with [[clang::musttail]], and corrupt src/dst pointers. + // (https://github.com/llvm/llvm-project/issues/70849) + // - Wasm tail-calls were only introduced in 2023 and aren't a mainstream feature yet. + // - Clang 18 runs into an ICE on armv7/androideabi with [[clang::musttail]]. + // (http://crbug.com/1504548) + // - Android RISC-V also runs into an ICE (b/314692534) + // - LoongArch developers indicate they had to turn it off + // - Windows builds generate incorrect code with [[clang::musttail]] and crash mysteriously. + // (http://crbug.com/1505442) + #if __has_cpp_attribute(clang::musttail) && !__has_feature(memory_sanitizer) \ + && !__has_feature(address_sanitizer) \ + && !defined(__EMSCRIPTEN__) \ + && !defined(__arm__) \ + && !defined(__riscv) \ + && !defined(__loongarch__) \ + && !defined(_WIN32) && !defined(__SYMBIAN32__) + #define SKCMS_HAS_MUSTTAIL 1 + #endif + #endif +#endif + +#ifndef SKCMS_FALLTHROUGH + #define SKCMS_FALLTHROUGH +#endif +#ifndef SKCMS_HAS_MUSTTAIL + #define SKCMS_HAS_MUSTTAIL 0 +#endif + +#if defined(__clang__) + #define SKCMS_MAYBE_UNUSED __attribute__((unused)) + #pragma clang diagnostic ignored "-Wused-but-marked-unused" +#elif defined(__GNUC__) + #define SKCMS_MAYBE_UNUSED __attribute__((unused)) +#elif defined(_MSC_VER) + #define SKCMS_MAYBE_UNUSED __pragma(warning(suppress:4100)) +#else + #define SKCMS_MAYBE_UNUSED +#endif + +// sizeof(x) will return size_t, which is 32-bit on some machines and 64-bit on others. +// We have better testing on 64-bit machines, so force 32-bit machines to behave like 64-bit. +// +// Please do not use sizeof() directly, and size_t only when required. +// (We have no way of enforcing these requests...) +#define SAFE_SIZEOF(x) ((uint64_t)sizeof(x)) + +// Same sort of thing for _Layout structs with a variable sized array at the end (named "variable"). +#define SAFE_FIXED_SIZE(type) ((uint64_t)offsetof(type, variable)) + +// If this isn't Clang, GCC, or Emscripten with SIMD support, we are in SKCMS_PORTABLE mode. +#if !defined(SKCMS_PORTABLE) && !(defined(__clang__) || \ + defined(__GNUC__) || \ + (defined(__EMSCRIPTEN__) && defined(__wasm_simd128__))) + #define SKCMS_PORTABLE 1 +#endif + +// If we are in SKCMS_PORTABLE mode or running on a non-x86-64 platform, we can't enable HSW or SKX. +// We also disable HSW/SKX on Android, even if it's Android on x64, since it's unlikely to benefit. +#if defined(SKCMS_PORTABLE) || !defined(__x86_64__) || defined(ANDROID) || defined(__ANDROID__) + #undef SKCMS_FORCE_HSW + #if !defined(SKCMS_DISABLE_HSW) + #define SKCMS_DISABLE_HSW 1 + #endif + + #undef SKCMS_FORCE_SKX + #if !defined(SKCMS_DISABLE_SKX) + #define SKCMS_DISABLE_SKX 1 + #endif +#endif + +// ~~~~ Shared ~~~~ +typedef struct skcms_ICCTag { + uint32_t signature; + uint32_t type; + uint32_t size; + const uint8_t* buf; +} skcms_ICCTag; + +typedef struct skcms_ICCProfile skcms_ICCProfile; +typedef struct skcms_TransferFunction skcms_TransferFunction; +typedef union skcms_Curve skcms_Curve; + +void skcms_GetTagByIndex (const skcms_ICCProfile*, uint32_t idx, skcms_ICCTag*); +bool skcms_GetTagBySignature(const skcms_ICCProfile*, uint32_t sig, skcms_ICCTag*); + +float skcms_MaxRoundtripError(const skcms_Curve* curve, const skcms_TransferFunction* inv_tf); + +// 252 of a random shuffle of all possible bytes. +// 252 is evenly divisible by 3 and 4. Only 192, 10, 241, and 43 are missing. +// Used for ICC profile equivalence testing. +extern const uint8_t skcms_252_random_bytes[252]; + +// ~~~~ Portable Math ~~~~ +static inline float floorf_(float x) { + float roundtrip = (float)((int)x); + return roundtrip > x ? roundtrip - 1 : roundtrip; +} +static inline float fabsf_(float x) { return x < 0 ? -x : x; } +float powf_(float, float); + +#ifdef __cplusplus +} +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_public.h b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_public.h new file mode 100644 index 00000000000..3510f89ef8b --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/src/skcms_public.h @@ -0,0 +1,406 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#pragma once + +// skcms_public.h contains the entire public API for skcms. + +#ifndef SKCMS_API + #define SKCMS_API +#endif + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// A row-major 3x3 matrix (ie vals[row][col]) +typedef struct skcms_Matrix3x3 { + float vals[3][3]; +} skcms_Matrix3x3; + +// It is _not_ safe to alias the pointers to invert in-place. +SKCMS_API bool skcms_Matrix3x3_invert(const skcms_Matrix3x3*, skcms_Matrix3x3*); +SKCMS_API skcms_Matrix3x3 skcms_Matrix3x3_concat(const skcms_Matrix3x3*, const skcms_Matrix3x3*); + +// A row-major 3x4 matrix (ie vals[row][col]) +typedef struct skcms_Matrix3x4 { + float vals[3][4]; +} skcms_Matrix3x4; + +// A transfer function mapping encoded values to linear values, +// represented by this 7-parameter piecewise function: +// +// linear = sign(encoded) * (c*|encoded| + f) , 0 <= |encoded| < d +// = sign(encoded) * ((a*|encoded| + b)^g + e), d <= |encoded| +// +// (A simple gamma transfer function sets g to gamma and a to 1.) +typedef struct skcms_TransferFunction { + float g, a,b,c,d,e,f; +} skcms_TransferFunction; + +SKCMS_API float skcms_TransferFunction_eval (const skcms_TransferFunction*, float); +SKCMS_API bool skcms_TransferFunction_invert(const skcms_TransferFunction*, + skcms_TransferFunction*); + +typedef enum skcms_TFType { + skcms_TFType_Invalid, + skcms_TFType_sRGBish, + skcms_TFType_PQish, + skcms_TFType_HLGish, + skcms_TFType_HLGinvish, +} skcms_TFType; + +// Identify which kind of transfer function is encoded in an skcms_TransferFunction +SKCMS_API skcms_TFType skcms_TransferFunction_getType(const skcms_TransferFunction*); + +// We can jam a couple alternate transfer function forms into skcms_TransferFunction, +// including those matching the general forms of the SMPTE ST 2084 PQ function or HLG. +// +// PQish: +// max(A + B|encoded|^C, 0) +// linear = sign(encoded) * (------------------------) ^ F +// D + E|encoded|^C +SKCMS_API bool skcms_TransferFunction_makePQish(skcms_TransferFunction*, + float A, float B, float C, + float D, float E, float F); +// HLGish: +// { K * sign(encoded) * ( (R|encoded|)^G ) when 0 <= |encoded| <= 1/R +// linear = { K * sign(encoded) * ( e^(a(|encoded|-c)) + b ) when 1/R < |encoded| +SKCMS_API bool skcms_TransferFunction_makeScaledHLGish(skcms_TransferFunction*, + float K, float R, float G, + float a, float b, float c); + +// Compatibility shim with K=1 for old callers. +static inline bool skcms_TransferFunction_makeHLGish(skcms_TransferFunction* fn, + float R, float G, + float a, float b, float c) { + return skcms_TransferFunction_makeScaledHLGish(fn, 1.0f, R,G, a,b,c); +} + +// PQ mapping encoded [0,1] to linear [0,1]. +static inline bool skcms_TransferFunction_makePQ(skcms_TransferFunction* tf) { + return skcms_TransferFunction_makePQish(tf, -107/128.0f, 1.0f, 32/2523.0f + , 2413/128.0f, -2392/128.0f, 8192/1305.0f); +} +// HLG mapping encoded [0,1] to linear [0,12]. +static inline bool skcms_TransferFunction_makeHLG(skcms_TransferFunction* tf) { + return skcms_TransferFunction_makeHLGish(tf, 2.0f, 2.0f + , 1/0.17883277f, 0.28466892f, 0.55991073f); +} + +// Is this an ordinary sRGB-ish transfer function, or one of the HDR forms we support? +SKCMS_API bool skcms_TransferFunction_isSRGBish(const skcms_TransferFunction*); +SKCMS_API bool skcms_TransferFunction_isPQish (const skcms_TransferFunction*); +SKCMS_API bool skcms_TransferFunction_isHLGish (const skcms_TransferFunction*); + +// Unified representation of 'curv' or 'para' tag data, or a 1D table from 'mft1' or 'mft2' +typedef union skcms_Curve { + struct { + uint32_t alias_of_table_entries; + skcms_TransferFunction parametric; + }; + struct { + uint32_t table_entries; + const uint8_t* table_8; + const uint8_t* table_16; + }; +} skcms_Curve; + +// Complex transforms between device space (A) and profile connection space (B): +// A2B: device -> [ "A" curves -> CLUT ] -> [ "M" curves -> matrix ] -> "B" curves -> PCS +// B2A: device <- [ "A" curves <- CLUT ] <- [ "M" curves <- matrix ] <- "B" curves <- PCS + +typedef struct skcms_A2B { + // Optional: N 1D "A" curves, followed by an N-dimensional CLUT. + // If input_channels == 0, these curves and CLUT are skipped, + // Otherwise, input_channels must be in [1, 4]. + uint32_t input_channels; + skcms_Curve input_curves[4]; + uint8_t grid_points[4]; + const uint8_t* grid_8; + const uint8_t* grid_16; + + // Optional: 3 1D "M" curves, followed by a color matrix. + // If matrix_channels == 0, these curves and matrix are skipped, + // Otherwise, matrix_channels must be 3. + uint32_t matrix_channels; + skcms_Curve matrix_curves[3]; + skcms_Matrix3x4 matrix; + + // Required: 3 1D "B" curves. Always present, and output_channels must be 3. + uint32_t output_channels; + skcms_Curve output_curves[3]; +} skcms_A2B; + +typedef struct skcms_B2A { + // Required: 3 1D "B" curves. Always present, and input_channels must be 3. + uint32_t input_channels; + skcms_Curve input_curves[3]; + + // Optional: a color matrix, followed by 3 1D "M" curves. + // If matrix_channels == 0, this matrix and these curves are skipped, + // Otherwise, matrix_channels must be 3. + uint32_t matrix_channels; + skcms_Matrix3x4 matrix; + skcms_Curve matrix_curves[3]; + + // Optional: an N-dimensional CLUT, followed by N 1D "A" curves. + // If output_channels == 0, this CLUT and these curves are skipped, + // Otherwise, output_channels must be in [1, 4]. + uint32_t output_channels; + uint8_t grid_points[4]; + const uint8_t* grid_8; + const uint8_t* grid_16; + skcms_Curve output_curves[4]; +} skcms_B2A; + +typedef struct skcms_CICP { + uint8_t color_primaries; + uint8_t transfer_characteristics; + uint8_t matrix_coefficients; + uint8_t video_full_range_flag; +} skcms_CICP; + +typedef struct skcms_ICCProfile { + const uint8_t* buffer; + + uint32_t size; + uint32_t data_color_space; + uint32_t pcs; + uint32_t tag_count; + + // skcms_Parse() will set commonly-used fields for you when possible: + + // If we can parse red, green and blue transfer curves from the profile, + // trc will be set to those three curves, and has_trc will be true. + bool has_trc; + skcms_Curve trc[3]; + + // If this profile's gamut can be represented by a 3x3 transform to XYZD50, + // skcms_Parse() sets toXYZD50 to that transform and has_toXYZD50 to true. + bool has_toXYZD50; + skcms_Matrix3x3 toXYZD50; + + // If the profile has a valid A2B0 or A2B1 tag, skcms_Parse() sets A2B to + // that data, and has_A2B to true. skcms_ParseWithA2BPriority() does the + // same following any user-provided prioritization of A2B0, A2B1, or A2B2. + bool has_A2B; + skcms_A2B A2B; + + // If the profile has a valid B2A0 or B2A1 tag, skcms_Parse() sets B2A to + // that data, and has_B2A to true. skcms_ParseWithA2BPriority() does the + // same following any user-provided prioritization of B2A0, B2A1, or B2A2. + bool has_B2A; + skcms_B2A B2A; + + // If the profile has a valid CICP tag, skcms_Parse() sets CICP to that data, + // and has_CICP to true. + bool has_CICP; + skcms_CICP CICP; +} skcms_ICCProfile; + +// The sRGB color profile is so commonly used that we offer a canonical skcms_ICCProfile for it. +SKCMS_API const skcms_ICCProfile* skcms_sRGB_profile(void); +// Ditto for XYZD50, the most common profile connection space. +SKCMS_API const skcms_ICCProfile* skcms_XYZD50_profile(void); + +SKCMS_API const skcms_TransferFunction* skcms_sRGB_TransferFunction(void); +SKCMS_API const skcms_TransferFunction* skcms_sRGB_Inverse_TransferFunction(void); +SKCMS_API const skcms_TransferFunction* skcms_Identity_TransferFunction(void); + +// Practical equality test for two skcms_ICCProfiles. +// The implementation is subject to change, but it will always try to answer +// "can I substitute A for B?" and "can I skip transforming from A to B?". +SKCMS_API bool skcms_ApproximatelyEqualProfiles(const skcms_ICCProfile* A, + const skcms_ICCProfile* B); + +// Practical test that answers: Is curve roughly the inverse of inv_tf? Typically used by passing +// the inverse of a known parametric transfer function (like sRGB), to determine if a particular +// curve is very close to sRGB. +SKCMS_API bool skcms_AreApproximateInverses(const skcms_Curve* curve, + const skcms_TransferFunction* inv_tf); + +// Similar to above, answering the question for all three TRC curves of the given profile. Again, +// passing skcms_sRGB_InverseTransferFunction as inv_tf will answer the question: +// "Does this profile have a transfer function that is very close to sRGB?" +SKCMS_API bool skcms_TRCs_AreApproximateInverse(const skcms_ICCProfile* profile, + const skcms_TransferFunction* inv_tf); + +// Parse an ICC profile and return true if possible, otherwise return false. +// Selects an A2B profile (if present) according to priority list (each entry 0-2). +// The buffer is not copied; it must remain valid as long as the skcms_ICCProfile will be used. +SKCMS_API bool skcms_ParseWithA2BPriority(const void*, size_t, + const int priority[], int priorities, + skcms_ICCProfile*); + +static inline bool skcms_Parse(const void* buf, size_t len, skcms_ICCProfile* profile) { + // For continuity of existing user expectations, + // prefer A2B0 (perceptual) over A2B1 (relative colormetric), and ignore A2B2 (saturation). + const int priority[] = {0,1}; + return skcms_ParseWithA2BPriority(buf, len, + priority, sizeof(priority)/sizeof(*priority), + profile); +} + +SKCMS_API bool skcms_ApproximateCurve(const skcms_Curve* curve, + skcms_TransferFunction* approx, + float* max_error); + +SKCMS_API bool skcms_GetCHAD(const skcms_ICCProfile*, skcms_Matrix3x3*); +SKCMS_API bool skcms_GetWTPT(const skcms_ICCProfile*, float xyz[3]); + +// These are common ICC signature values +enum { + // data_color_space + skcms_Signature_CMYK = 0x434D594B, + skcms_Signature_Gray = 0x47524159, + skcms_Signature_RGB = 0x52474220, + + // pcs + skcms_Signature_Lab = 0x4C616220, + skcms_Signature_XYZ = 0x58595A20, +}; + +typedef enum skcms_PixelFormat { + skcms_PixelFormat_A_8, + skcms_PixelFormat_A_8_, + skcms_PixelFormat_G_8, + skcms_PixelFormat_G_8_, + + skcms_PixelFormat_RGB_565, + skcms_PixelFormat_BGR_565, + + skcms_PixelFormat_ABGR_4444, + skcms_PixelFormat_ARGB_4444, + + skcms_PixelFormat_RGB_888, + skcms_PixelFormat_BGR_888, + skcms_PixelFormat_RGBA_8888, + skcms_PixelFormat_BGRA_8888, + skcms_PixelFormat_RGBA_8888_sRGB, // Automatic sRGB encoding / decoding. + skcms_PixelFormat_BGRA_8888_sRGB, // (Generally used with linear transfer functions.) + + skcms_PixelFormat_RGBA_1010102, + skcms_PixelFormat_BGRA_1010102, + + skcms_PixelFormat_RGB_161616LE, // Little-endian. Pointers must be 16-bit aligned. + skcms_PixelFormat_BGR_161616LE, + skcms_PixelFormat_RGBA_16161616LE, + skcms_PixelFormat_BGRA_16161616LE, + + skcms_PixelFormat_RGB_161616BE, // Big-endian. Pointers must be 16-bit aligned. + skcms_PixelFormat_BGR_161616BE, + skcms_PixelFormat_RGBA_16161616BE, + skcms_PixelFormat_BGRA_16161616BE, + + skcms_PixelFormat_RGB_hhh_Norm, // 1-5-10 half-precision float in [0,1] + skcms_PixelFormat_BGR_hhh_Norm, // Pointers must be 16-bit aligned. + skcms_PixelFormat_RGBA_hhhh_Norm, + skcms_PixelFormat_BGRA_hhhh_Norm, + + skcms_PixelFormat_RGB_hhh, // 1-5-10 half-precision float. + skcms_PixelFormat_BGR_hhh, // Pointers must be 16-bit aligned. + skcms_PixelFormat_RGBA_hhhh, + skcms_PixelFormat_BGRA_hhhh, + + skcms_PixelFormat_RGB_fff, // 1-8-23 single-precision float (the normal kind). + skcms_PixelFormat_BGR_fff, // Pointers must be 32-bit aligned. + skcms_PixelFormat_RGBA_ffff, + skcms_PixelFormat_BGRA_ffff, + + skcms_PixelFormat_RGB_101010x_XR, // Note: This is located here to signal no clamping. + skcms_PixelFormat_BGR_101010x_XR, // Compatible with MTLPixelFormatBGR10_XR. + skcms_PixelFormat_RGBA_10101010_XR, // Note: This is located here to signal no clamping. + skcms_PixelFormat_BGRA_10101010_XR, // Compatible with MTLPixelFormatBGRA10_XR. +} skcms_PixelFormat; + +// We always store any alpha channel linearly. In the chart below, tf-1() is the inverse +// transfer function for the given color profile (applying the transfer function linearizes). + +// We treat opaque as a strong requirement, not just a performance hint: we will ignore +// any source alpha and treat it as 1.0, and will make sure that any destination alpha +// channel is filled with the equivalent of 1.0. + +// We used to offer multiple types of premultiplication, but now just one, PremulAsEncoded. +// This is the premul you're probably used to working with. + +typedef enum skcms_AlphaFormat { + skcms_AlphaFormat_Opaque, // alpha is always opaque + // tf-1(r), tf-1(g), tf-1(b), 1.0 + skcms_AlphaFormat_Unpremul, // alpha and color are unassociated + // tf-1(r), tf-1(g), tf-1(b), a + skcms_AlphaFormat_PremulAsEncoded, // premultiplied while encoded + // tf-1(r)*a, tf-1(g)*a, tf-1(b)*a, a +} skcms_AlphaFormat; + +// Convert npixels pixels from src format and color profile to dst format and color profile +// and return true, otherwise return false. It is safe to alias dst == src if dstFmt == srcFmt. +SKCMS_API bool skcms_Transform(const void* src, + skcms_PixelFormat srcFmt, + skcms_AlphaFormat srcAlpha, + const skcms_ICCProfile* srcProfile, + void* dst, + skcms_PixelFormat dstFmt, + skcms_AlphaFormat dstAlpha, + const skcms_ICCProfile* dstProfile, + size_t npixels); + +// If profile can be used as a destination in skcms_Transform, return true. Otherwise, attempt to +// rewrite it with approximations where reasonable. If successful, return true. If no reasonable +// approximation exists, leave the profile unchanged and return false. +SKCMS_API bool skcms_MakeUsableAsDestination(skcms_ICCProfile* profile); + +// If profile can be used as a destination with a single parametric transfer function (ie for +// rasterization), return true. Otherwise, attempt to rewrite it with approximations where +// reasonable. If successful, return true. If no reasonable approximation exists, leave the +// profile unchanged and return false. +SKCMS_API bool skcms_MakeUsableAsDestinationWithSingleCurve(skcms_ICCProfile* profile); + +// Returns a matrix to adapt XYZ color from given the whitepoint to D50. +SKCMS_API bool skcms_AdaptToXYZD50(float wx, float wy, + skcms_Matrix3x3* toXYZD50); + +// Returns a matrix to convert RGB color into XYZ adapted to D50, given the +// primaries and whitepoint of the RGB model. +SKCMS_API bool skcms_PrimariesToXYZD50(float rx, float ry, + float gx, float gy, + float bx, float by, + float wx, float wy, + skcms_Matrix3x3* toXYZD50); + +// Call before your first call to skcms_Transform() to skip runtime CPU detection. +SKCMS_API void skcms_DisableRuntimeCPUDetection(void); + +// Utilities for programmatically constructing profiles +static inline void skcms_Init(skcms_ICCProfile* p) { + memset(p, 0, sizeof(*p)); + p->data_color_space = skcms_Signature_RGB; + p->pcs = skcms_Signature_XYZ; +} + +static inline void skcms_SetTransferFunction(skcms_ICCProfile* p, + const skcms_TransferFunction* tf) { + p->has_trc = true; + for (int i = 0; i < 3; ++i) { + p->trc[i].table_entries = 0; + p->trc[i].parametric = *tf; + } +} + +static inline void skcms_SetXYZD50(skcms_ICCProfile* p, const skcms_Matrix3x3* m) { + p->has_toXYZD50 = true; + p->toXYZD50 = *m; +} + +#ifdef __cplusplus +} +#endif diff --git a/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/version.sha1 b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/version.sha1 new file mode 100755 index 00000000000..aac43a8c6eb --- /dev/null +++ b/Tests/LottieMetalTest/skia/PublicHeaders/skia/modules/skcms/version.sha1 @@ -0,0 +1 @@ +5d9221d28f9cbbe6db0b745b36d4e5efc09e168e diff --git a/Tests/LottieMetalTest/skia/device/libskia.framework/Info.plist b/Tests/LottieMetalTest/skia/device/libskia.framework/Info.plist new file mode 100644 index 0000000000000000000000000000000000000000..ac23c3e15d0f5ea176cec4ab46168d1312d3c5bd GIT binary patch literal 756 zcmZWl%Wl&^6rCAf;ZZVaXd#922oxwF%abTPR!E5nib~oLI}M)VqsEFVf z`Voaa3%-D5zXBGB1rlP(nz6Adg4x`ebI(0<&l&lgg^?_O#tRS~I(+2lvBL2aCr{0t zF5=RB`OL!N*>jbp^B0ycUbxM(0E1IgTCAXoXJu^Lv zzDSW~%($iFO3qd24&|QjP&evRn|ZE|i+OL824RYn)2XguPMAwK)bzEjXgzL3V=r)$ zUvyl~$9jf2HEgQQe1R_Is5~)vg3|Dg_)69#Rz79 zmYUD|k6uwTj*JmcEVewyXPgE)#$DzzCw4Z}|3^BO@XN-G2HS&}1 zRc5A-8IwUvdM#aTQcq{xu7Q8;CWNQLJPl&~YXnUS5n8|K$Z`{NXBkiF$8fi$iO;T?WSq25r08Q`&Xg~+A!7g|Q zK7f7j6(U%L_h1ta;7d4zyKoP_hacfS`~<(iZ}2<(0e_+bnnNX2Mk{C)t)o7&kc}9c TA|J&dJqZ4;;7(Svg5LZBTE6EC literal 0 HcmV?d00001 diff --git a/Tests/LottieMetalTest/skia/device/libskia.framework/libskia b/Tests/LottieMetalTest/skia/device/libskia.framework/libskia new file mode 100755 index 0000000000000000000000000000000000000000..e342d3df442879fc3c03f0661c528cdf099c0d4d GIT binary patch literal 2954736 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≷y=24c*5Jxvm7?M)Ez7d9}$K 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 z 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 (+YaFOue)>x%$yfi`ZHkGX9FSHl1H0Z$>Y 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%0D;&Uz_w5eglvzG&C-&ml1kK0c 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

)#g7u=O^ZrRT_OU&z4|G@ujI6f8Pzh<@fU2R1% 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�pm+^m99sfz6 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 z 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 $>E6p)#58+XOQ?P_7C{9tbTcH ziK>X-afJ@;J& z-1C8d9{re$J;96%rGEx*84kZs2!3T;>7&?CBz7bT-G}gvt<)XEvr=Ph@9ZpPY&9*Z zTEN3*r)ZzNwq3>e-s$iTN6(_FY~n~`dSa`dBi?5w;~@IHa2_Q7TGZJmR}Gc- zDcqmaqS=YitVOE<+J3G8o)n_Vhj~|T%+=&-hufRgM!eerV&4u@PIz6xHT`9Ueyy@L z=C#x%YqR(aJtTY%8W1`>1g+GE_<+0@+FcFar2H-LNTFLPYtd~vb_Sto^IX#e|5KC= zU*jcLk+seX9m#w(=B)k9bKe~Tc1Kw5yC3~W$gfW$_Z<5cwQgYUyX$!``gkk%-ERDH zbO-ua_-u)>4kY=%(TUt1mjCWD_2FwYaX{&qM_by+biGv`W?oV>sR0d zvJS4fR5jH_-@hFl%Jw?@4X+hnteS%dRZYq^^79gBT_tVS5{Uo)$_cw<(@_xCUl4$tEm`4;?o&!YW7e5C()vUq+MM137n4uuZ(LSESC!`CLFS)+OV?-(LBR)g zYW!~cF?^SILZgvWx{dhw;3dRG;+Kd2Uej*$m+~(5qHgg~Hty577krEbcfloBG^A8& z29I9PoDP5wiSPKPPVDme@C0HlQ&}sGF}YD?jg7^}GaWtQF4Y~Jh#vy^50v=2ptmaF zdnq@C@jS-7lChP3{qPm!>9GFpZ?J88mDub0JiZU(>B0v@MiJ=e)afxW?)ZW1`Kuh@Bfr{b$r*zRr>#r(2%*C?*%bB*E3-k~m@>xEp$a7C7{OXfP2 zYZ}+fRI|{r4sF)kQ}MTJxxB9O@fr2K(?c{lwT~u+512GrJ0wjWgC74BqDdY2ESkI% z+yGZs%0iWu3{5g8I(LwlKxlF)?_?}XnNtD$6NH9Kc~*??c^fn#J_uJZUnL*3jB!rb z7^}m*uU0W8&x3PhQ_e4l0-w>~RR_Ng@Eikgh-DundRNY%^O|FqgO7#q3bnbSsETi8 zE>t}1%{s_gK)yVoOME`@6RHZ)jmV@a_yncmn=7_LvBOIYOE0lE>ybf^$EXFHp#jOc zF@5D{wSQjPxTRp8NptNfThp?z@d({!+SjM8jZ~BWXLaKi??bs+GS~8{FZ0#z+WgpA z4Mxrmo{2xL#4J~@hOhAd;~#a`s_#`4eFIo!KFMC{<9O!a5`i_@kH7XN`Sw-XK29uC z6|m@9dbOVGHFBL8>ksdRel+%8vgYe_Z?gwn3V)8Fe`~nvT&3Qv)GPlDI%FSY z(P7Vh^T6?3a6Jc{7l8XK@!Ob<9Bay1vdKpki;ULfoA)vPm-8;TT8`^h!)yxshnYNiRjiAs%B|d117nK>*uh|Wb5a88_37IC#&a* zjfXzbUS9gu?;Xin6|;q$ydAsYtFks%*&KT`=G+CGXHyFQb42-ye3@BWEPs8ecPP8H z$bLF{BG%8-w3B4CBXm|DYG(}XRE64kg?6g4MW-P#5efdiclXp{5A50+!<;#dtg1ny zBCBf15`*n(E|u~2;AdtZezvT0h^2=I`oiNAeRLz`K zKUX7#@!YCb%9uM``HZDwz*ssM%bCJY@FnqZK1POH9bU{_aKKl@uPK{+TMjK|&phOc z;-uEgCwj^45uvWOMXIYmz~|Ebz_!BTM`vQAc(RZL95s*deF^f5pEWk?o~*_!=28Z} z+1q#~Fd)MiwvK`q9w{n);f32hrTDgiqXMO-%lk#x1*_Q?g~7TXnk$;~)mLwa?$S9k zB72cntq>SvfH75ITz3CsYD7fgM4zg;75F6Qa*1Qj_9dftJ|8(5yV>Mzg}+Rl#r5gJ z=*dqN{ylYep*uCNaB=Dth3;_^3iHODS6H!r=JrQ`e|xSv?1qoyW7^?`4$33&m!kjK z;`cPm+qjkZk4Ii2Kkx)~4}CNGDsZDK@V7;(kKG$=HS>OZlJE>OHj3*$c|Ux7lzEns z$W}tLE#NJf$)0r8qAKbi?N}94TjmSiN7Do7gjm)l$(Lg6ujHG2^BdV`{r-mh23ezD zWxnqqR@;RvC2>d4=HVc5N2mL?F5BBIFg)?{$=Yp;S;M&(nDc-|cwC)~A8l!j`>G|~ zhHtU_7uhBKXZXsBY>iAE6gh}*WgI2{*u{K*;3;o|HUc?^=acv!plsk%WToy!RSxtM zj&73{L@sgT1NhVPyK6t@Cw_nMk0$vp$hz_JPx+Sn4;oLhm+wDk#7)DVm%ebMe*SNd zRAqm3q~W>y8a$f*8^;Te?0fx!=Z};<|KCR(=l%W29L@0?V{aLyUX^u1#%kMmn^`C0 zZq%u?!>q>(EqSBfp3>M?Hs35Ov|yASMf>ovZP%L5?|kWGt&2S&zPgP;Tj$h_Tzw)}qy8IhoJG4YKxpRL#qb>iz8SdQOTp!~4I@c<$bGzNG5#52U zs=2{_T_br3n>?e`Wbsv&TvI!kLk3Lj*{ro&;Hj_t5800m$iLHuZ+g#t{d=+Q+ZOcg z!(_}FntN*5AC4B7-^E}25V^nG5=8Hq==aheHG+NbmB{$W^-sz>LnoQ&&zHW&sY_)( zS@;nh_~7x;@C6S<&(1k8^2!`VPFS%sD(uoq$5!QXIiuJw_I>|U=zHkXz^_Xk*4V?e z5pN^){sB7k>1t|#|3P3+_jE(wc?Jzzbx1k+R`O3}5ZjOoeF*&MZ6^yIw=(x+o@(oJ z8-n9ijTf9EBk%iYF?GSyc4G$%lP)uDj3^?F7!jdyJij zPkte|jxm=1D!6_Sf@|wx;hp`l(ceE}OO&Gm30wb- zSZVA3P;!kzu90t*su)n#k{#51a%SmQ<=C&6ypgKeg|AB{ekSAbsc|u1P6zKPn=4Y7HjOC zI@kx48+)hNEfetrm-E+_Ry@2}p3nPg;K)qkmaZoUUMjSdLB6sA_>>-Q&8J~E*E#@8)e;4Juyyte}gYRcw?omw{qg0n_k=WG4jyBm}!%vE%RM%|2 z7k!$1|LP6;mfElBTMYexu@^P#=@_z}6r&#bPG6&M`J+v*URmTt{vO*AKu;|(f-?WU z&rj-;J-`&`s|tGGYIAlL*qxoewSS#awRTZsL1jjxxAOmTK2)-we1jb#H{N-Jx8bgD zs)l>m>qy*+%=fGK{#|@rT&z7U)WbgGtmn5?XCgQg9E8jJB7aFvoevM&5{G&&^brBx z)4$nWn{~&@6wAha-W@{Yt@ZEn-Ei`$>C>qDJ$2scYuKlhE>_dmJgGG%yNIt+wi?d` z=wfH(?SGwlogz4$Fm;Q{@{-^8e0)`8{YdgZ(Jpb$-X8pCda5S%mzS~*`0^Zd9c9pQ z8Fky(m-S+UyOjUd_hREX@L_kYe2bh<&M)sWJAN^x$gL3-vwN0Y@$@9)e%Ij==aYu}tLxw-3w%rmA8BJblSg@1PL1%cl)b^M`@3t6b420CW(-e?f8C3$ae^bB ziSF+JIc#L@+ZX}9csA<4y3v=zuj&7ru{;x>j*+$;kr@TAqEC9uX5g0Gjx8%CZ=%vN zomDaBy~h28|3H^V9fCVY;Cc3Ycy|2|`>C7ef_vx(JGrqZPtxfZe#)Z9 z((w7J!e2lihrYsgYK{!XW8YBE$#I5XSGX^ThAsttufT-QkofJ&*!?~zV~MW-Ixg+p zh~-t8=$YnlHdE^_XV}-l!+ujPoIR`zfQ>_YqaB0(pA`9R+1@jgGt(s5Cj(PxY9 zq@D%Tqm)LxpXswh4sgH5a~tgl?Omd}$u%^zKJ0`&k}xyo5J`h9C2Yaj+r{>wQ=?)7VOv@AEi`B#dgkF$-s#P7@S2X?!hrTmw&0m=q!s##!;6}uiV8)fiaqpccQtG(2}?cUPYBxmP* z+AiT+5ADj@TLrzj*fYqwww3!jo41*H0KE;=1&Cfw)|}qTNc{EU@NFA`k6S$cdLzky zKZ><#GoM?uoV@s-G6p9YgD%ES zXqY`}e;yQaFa&t_(L3nb?s1NheTBe{eXi(RHq(|6{B}?6pQF_)UO88k+{0?r1qBJ#^ve~|WG=hscF?oH&HwqW}$u(c#&BaNQNoSB~^=QFq>#yj1_QrD+GP=wCs zc3D^ROUFC)FPUd3$P$1(N=Tz@Q*SSyD zuW@&QgVE|z&ST2ymi)xxXDB$L9A%2S$&u8V58sz(T%(C0w(cXim)L+-D zNX~*lg6jEz+|;6z3`7?h?Z1zHJP>`YaX$rE>avK7XkiXe{*kO=r&Z@iv?q1Ckwc~Y zfssWyBN&HC_-Fdax!;!H?MY^C)Vs|w+0Atuv59iE#zWwJ9Gs7|dz+VnbFceCXYN%O zI@_m=wKFfRY`I0)UG9QG>NFAcD|0Q#`E#2EZ(&Ki<2%hAg zHjKKVHS4>-@?9x)iw>-S`m2ZwA8A+3rRAR20>EBEJ%KIWW{W1o2U#nHM?e#D_KI8d z1+)>^V$ww0L2|A#7G82U9JtKeEaT&amfMc>E<7+(a>u%zE!=OEeJXVG73dJVdvp85 zJm|yQ#5}lE=(b5<9z;v)DeFyElfTM4#aU6a1XuK_4Y;4DA8zhtZma`$%AIY{eGd8} zMXn2(2LNJ}-9}k2eLj@Je3H5-n#A8}418|4IR;D4U<_nl-U!aY zYYA;-8gs&&pWnWxr&f5tV4O|{r**O7D>2spGxjE*^6f(Mbi3pkJUx7_&TKH}=1#7$ zz%K8;!+mj9wz2-qqpz}#w3DZ@HMDMgD143eM6s^SWbc^EIwI?l=t{Df{~pZ~BfL&x zIk63ti%#eQU=P7rbQAm|hPEb3d%!rC|M=_q$-lBd{Ly3bXtPYZzi3by>)ZMA9J(oCE(%TShAixXu02lcx!^=_`diA%)l2{~Noy>=oo2$-`}o z2|Qo=E&TJ(^*yzctKkAW{JWKP2);OiTrU3H#m*{phuE;nyP`Lxb%679-vdQkCN<)52mwQdW!0Ar)ILnbeO8B-II(&pP@?@;GqxXx4uOb7C zNzqcW?{OG8F{UymqXYNBqg~m~whKAWeJc3@Iaf^0&vxcs*kBfe^ z(a(>BzjHl6+rn$SoNIA__7Bp&)W3wEoING*xu#VV7Qf?K-bUL38~eXP+gonh#ut~i z$(40V+drXg_6G(ogVD8V0pOU0PEgJqY56m2F1R4?_oV{I;Bu0a;C$emN+0l%NK7FI ziGe5ZEb}0jF&6u7ChtFGETxZD|0G^gbjn6~qn{=4l*P;g8GrMe5BX+ro8RI)3pP2^ zLnk-+M_DC9!ghi&l=|MK9*^7Qoc9s_Mj!P~PW&i(syr7M-60s|T-Lpm6L`KY{2LgO zkflV|xhNJ}9dlitx#`Pz=7SEsRY7lZ22?S1fAZ_CX;SA>>X11Rb6XF2>Axm=@c}tG zT4l|lo}T-@icGi=+3;Fq#3E$H1^9McLr#vXks%9_C9fh*V*WB@OGD1z^Pz3iaI(9s ziN}x~Et|IF2oT*9Ia~ZU{TW?4F^+P+f#`T1LRaL&&#*KNn`tQaaX7iQh1{Gw<8#rU z6VHVVvEWEgt;njaHew>liz)ki6{mHpKi0(?IWfnfgCHL!^@j3o4lOUgo{4;b9I8^Y zM0eufd>DBxDi4{l|D3ugXiFjgx7sRmia9ezLyi?0)zm2@`yb_-Ivae!fgHqKw#Gx& zaQ%AZS=RR`)-%<5E^_aW$H;Sa4}8$|L-StNc{Rz`XGB0}^Ssy$+mvuu~HVOU* zX+!pBrf$?{=sCmpvISyO&rKKq-Q5lNCaNCC?gnh7m(c!3<{absimuql<2xj}f0MrY z_p!1+STxiA`~A!ncW?Am#aSKd&2cr?K~sav4%M#ed3U$85$+puEim0-ORp(C`K~TD z8XvgU$=Bzht+LgN^!4N*EM4u;1x|%sC5pC~%Wvv!V+}oo=r#)|TY7RUs9}x)pH){Y zc#=M=Z{=g}@9&3&2MZqQvqqm?D&uXzo&5g(fi2M$jh(k8-IzBXxnGrU=&ePcVU?3> zcwbHZA2BWGVsjcxoHYF@{h_x(*7a?SV+s1=ndp#R+!x2onL64Y1Lq-oODXS5>65Hw zLHq^Q<7?~C)b!FQ85exlc~^Qn=UbD%yX;c(faB+@lb=NPaK+RqIMmR~mOkcds=|k@ zY&K;yy;?iGXqt@CV%AFW0VvHh{oejr_#6?Z!gnXMBrsON7Y4uxY*mca>-;=gRa)tJ za*lTYxVZ3_?!Ikcd{A%xsJ)!gVC`Wy^xIj_>8#HeU4iNn-QQE(Yd&AmgV0AJKDxzE ziXV>JlYy^NUI_0tr5Sny!FxE)i@~|j(bi1w3UF)0Pe2O@NXZEbQz zs1Jvr385c&@sRYhl)fwlo&x4d!63RRVNsL``!~;gIz$Ay7o~)^r zMxREQbVjViX!!=eiUnKAeHH^Obi5mW%2_KNE#Lq@j4pD!8-Ag^-0Q+apkH|&WSn$-B}`p-ieY1x^-9)q z?}gp9?P7P|mdSid^gAMLouUV>6a8)^zVYOzQ4ZhbA`=^SrwX+lUVOPPPpkQI09)d0 z!zboNiS_f%RXtk^(G3@=9jv1+j*<`;b!SFveTJV|a;?t|E_1wNU} z#@fTTl3$heVj;BtN~582zL0d;B>(InN)_pML#F z{eOOVq@CP_;?J>u(xdwtBK=3Qmi_mU^)LLx@aH&0Kj-1+5&J;>kq(~E%WXR{SBo|M zJUCAt9{LLF4QFP5^A!H#WL8DO8oMzhuk1mF2z>#G$sus_&e*- z|0z|}9t&SUe|PBFt(*xLlbykRAYpwPxzbW*3Z5#5imNs24#%0$X9E5QvIe$-JF)eM ze_FY$M}jlM--F!V{}1q3AHw61bizI*Jcft<8Z=_z@&)kZ8U}~Y^3KAu&KaXa;W7v= z3%YwJ%RW;h?yUm*ibX>P{e=08wBe_84Bfyce5E{$-Er1{9mFZg+8+Lm_@0R$l+fd* zJXOOu)MVgq;)4&rD?SHPx2FA)@%!DO;pP3_%E{!&?mve_c+NWV(`Ip8-(UZ{Q{SM5TLbIv`S!z}03#vn&ShqsHrIXOOW&mGMf+2rYv=hSg~ z9{-Os_htUygzqfA{V$$Cb`<}ozVB7ax6yBd|FreG*XTd&vHCwX7VB5=SEKWAlB{9mL;|`5Xg3@EZF-$)jMM(I)ow{v1PMQ`!)P zj45-z9h?HI!8cFNZz}s#;&C12v5bjapFn!npc6Pi{;Dyddm!6F4gWJ#iECU|TUQe|m- zO-25f7<%OY?{SWy%+pQGWAXdh3GRK2jqfx*Q`_JpmJBRocNkt$S`=!7I&Y?pV%Gng zLv$4`n@!H??(>=04L=b(;$!X5T4jHXsq&$_eSrPD7vDkyhgyz>zc;B6n;t)>J!`ybiQ2pmEyNBFKdQ}U>*n$g6%3;qJgA3ce?ja`kI9A1{v$S82e;mKI_RF6O0Y{SoX;&{p-csnd-3EC(HOYi@99NcUgQV z^{92;J|3TiZ+ZoL=)1sgFZJP5*ki(%WbjTId!e&>Vhd&MSP5Nxd8A%_A`-ql=}R^D zz&BDmCKD&65~qXnndAZYBD1)`yTyOS{-ub=G~yU3Bl@Kg8Qrx9kq6DTgvX@%*NdM8 z&suth_U|QLYT?dt_>A!T8?=!XWKIq*f#CmF8q0-Lcf2%XV_Ks%uCLHfQMzT?1WVX*I`;AG+;oLu%f zI0?+}kMAy%b6esZ)zU_Am^Rp>3-9l0KtG$gYSl*Bql^F3JHdgtI=0!iY4;#p4Hfro ztbu2BmW%QiGp7x?rFFb1x71y1oXhotBJr>4mt6+y&jhavhWYlV%|zN1Ui*z} zrOibDRO8y;_MQ7a8+0~QNY)C$Y<+)K==<{reSgy#et&uB`;^f4U)v`!jHce}zu&hd zwjw(Yl;1*rP{4ob!p~|Y5660P5a=Tu)d`$mWPK~R)2}B-&TEe}wZq3JGA~4D6i2zM zH1fEhmytd9TmI0@WJ^5X4eO6lryrN4$Z`3i7I>TW;m8Y5g4X;E2lKBl+Scwr4BytDXiN6hgWEbA99?~u zII?gfdl@UI%TPFq|J$i>4~BCvPR_LMm(HN>!DrA}b$0-_$Ln%B_zgufF6?I-zuz4k zNIMkI552BouX&I-a*08Iy^n6iPc_m1 zM{xJLLA#vQ2<<*czeENy{aDFMZ}7zaJyhqSU(_f5{j-#RH5_U0pFJE`y#KF<khy01@LT7n_Q!Ly2M4YJ#vBgi znVJ`BBYX`Q_4k4ESk{2>b^l^$docVK%+FEZ>3H1qL;rg`Zv27IuY0IDX~ABO{`V7X z=;1mP@fi{Rkjfae_l-fn?&LjaY!-cLH^w5B^L9=C5Uwu}ejvPWIGlADI13wp+8b)j zKWsmB%q{+E$^8Fp8-M#Zv=KamHiq?c2+{3x##ov0`_|gj$J@c*U_NfGQNquKU(X`1 zSvXIxHrA@4b$b6SPFJ2me@{o36T|v?x;|QcyPWpI`?kmETR#u_SJC62&Y)la+vE7& z#{>I%x^WyXZp@nh9R{AGnE#RR;Rx(_rXMyw@c3ezd>{=zkaeReE4QKB5TBug$O7*n z*ZvLK`a86h1wY8zA+m0oe-Zqkd{A*;tVBqjneT$r7G@QQ0g4dis!}C{T?O%n?igrb25E&qR zZGFyYyMH}3<=EGj4VGoB_R0-9viU8()VHn=!Poow624|f2J&~a-TrxNogMJc@}r2u z1Ka5sqJRHNesqEyK>rSYbpI#+-hN$!40a~{`rn@8o1vRCo#X#3?WCUp&;M7_&ZqxB z!Lh(`ra11t?Wgci4ZDI39}zqDx&EBi7LSk|G@rmjCHBXP`#Av*?SzMRh4IkzFuo}< zC8AF^{il=Rmr3x;MEGTb{~++0{4(9Y20lN}nA2vCbje>Wxh+4vB^UqFUm5F8xV}Sj zm8i!nikt^+o3QT}i63OlXPisI{wG}bYH}#mmC;S?GVz4M6F=|$ zVA_*?2s*zFy3|KqiNvWT64!VazA>`jGIHgk1C#TAo{{I@TpPmI=yCshE--NGCL%{Xj1GYr{>0EM6IXZSSHd*YO zCEu)Lo$A!%Ogd0rRr3;KHW;so3v-D*$1jL+6u$#GBVjCY+6gKlR-TK!%G_rT93Lq= z)bqjhCtaV*Iq%7azlF7*u45msXak*k`9#`%6+7xt!!N@;hjH-!Ny=MtK=`{O-{?MN z&nxrBYST;Ivd+u*?|s8;PxjV>+goGIse%2IZzPwEv>V>$A3vwf;;=TY@875GQtGQ$ z>Q!l5@S%a%q1qjaPYZ66|0B2IU0X^=9rauKnHB>^@i`NnSc1RZ=%d*VHpv<(FLueT zM*sR{7Hh9D+!`o;{9{XV8^pc{zoET>vaZP9F0la{zUv}y50r0z102Cne=R#@1#t8l za17kfS#mD^CBbqco#k+tMdv!J4C^P!Jcy*!h=5f0V;Sn5UVH)HpHMekj;-$O1S^~tLJEp4_3w=! zO0^>{wYr)<2`)vyKaaU3pzpq!iV@Uir?h=zhDPT_cqlZRC}e3 zb^L#F*EbqvUJFev>guhn7v9!74gV@~=us|qD>2V}@8D|ky&U76d_M!7tWv~&koQ(% z|1-JwaBt^+VD5naT$DEluxlCcB>ETgz32#~{X_3!*96~s$lpWeq3L5F_9pYJspHp? zb2Hy&_?F2&Y2f(}#92_+{u{7QOFbXLLqc^M^(@h*`>^L(&n{wZmGY%Gim!yZr?K`h z!`1!TS=PN^sJin{9jC#1ae=Fi{ahXG?H!u2I|1GLjAhfX!}g? zX6k*-Hf{b&+N>JTW@sE}vyL_o7;Opj(79%rPvuWKK^$X!NT;FZrran*pQEv&Z8_XVJf-pPIDz%c1)B zIe1Jti*`NefC=J!DQ`4erQDV?4l8p~z>f__)RpWE0YG||4S7g1jj2k9Z03QmUu z#-wvm6#JLKVfkHzAV9P%<-Z1+#V>lFVcr(iJTM$i&tNRapAKGle0Y1aL+zCg*Pi|K z>sJ$PohkqMS%}|VWwbR&)}UX)cf;j2OJ`}>@JxPd^Z(#9ZHhb)zBXI@_9vmXCx!LX z;Ioz-l`r|^qtQ#~%pC{3CIO?D7h8I3KCsOL#<{>c2bc@c zrN|kiW*&CVFi%1LAwP2jeTt*Z2+GFu%}Dw)ivC2@NPeA8c;_iH7jZ2cfjhueYzpFs zytD6rihmaLD{!>&{>#9!+u~V9-$HU3eXFByfy!T)dE0qc!oGP7-~Np`_MTnvFi>8* zMCg!mzoJfQ`xiz(uL0((fxVDETty$})5i!??%ic0H&_(Uqj|4Wj`LRUBmY1A*c_{& z?IuD)4;pJjls|?xWSp*}f1Uwr1Ni_#@}-x4{GD&i+ykfDSVZ4tt*f;L%yWTd4loq} z+m(zBYs4;dEq7oqBrajaqwJj=_*=Q~t@=NI{w&2syrQ9t3%wIxEXBJ(=$-f~g!|Qp z&xd_m*z;i5Fwdt_D?Vi3YWOAay~y|n?5ak5WB2{}vs&?MfRDd&kiJ^-zu4)X{Db6i zG-E3SKW|c}spByB&X%8mz&TVOri9=O*Whi``!a}$eh zGsdB61g5ei?=eV=^k#iaG3WGhX6c60ST;$oNpLsAGSxBGl+P#KlT^ z1^$EN!OrHJMeFe60p3MYj%ya*#7Mo=A$kFaX2Rjd=R@d_J+~2SZNZc^PK}A7%u;9& z-^rRq#5ov#b+jkX3CikwE_-h|t4{iNNcR4TEz>;2TMFEj0&gq#S!%^D;P7ba7dzHz z3uY#%6}!nxtA9gV;7~2oCsJljsLVvl+)A0Pl+nopQBIlC(Ry}(zBw*vnKoS6dg4w8 zl$|krS>mS$mYqL**&y*91M2;c;mcMrt^>;6I(%6V<2#`2{Vmfj01sP-z{756^;YmO z=h3kGcqX_i2Ulx|)6SRwjD1xIS2C{he-C3N@vk#isTF060b^~%e&;eDJ<2>cp`CHl z88>KT!J^Q(=|X$2LUU^UX7PW{N?N};ma$q2jEg1^2MxVRKK5HFH;*{3xvOHg%&oYf zdTy)Q3%zta0z4zewM=`S_uKhq2mLw-?QNsoc6-b`soSGHyxD@mf=g&nXec`j7U*}( zOaqpx&B27NHwYYM)HxG43c$Go`kfC9LVsB~ap_sSi>2*lp?343!*c3a(?o1Jco$lV zg_dsR{Z{Hg*RoLhkabaP`Ukvw^zxQz2F$$EsXuE z6O%4+pcee|fW<|=eAhVGEH{6QQSLkPt@Z4Bv)t|St@Uia`D~ed3oived~U8?TVIW!Ct8fA{9FH-`-<0)lP!2GI=!c~(3O zdxHl&mPqF&KsszBENd#!5)5mwT}`Bolg{rE%>z)*Q;p8Z6cgsuXpJ*jX7kMW6sR>I z9cRiwA>PmW?!6UBo9Vr-_j<4QdjI%d-_w0u_qx};?zPsvmaNU+9^;%GwBzT!eR~oW z@HT9|;bQVn+BoQ9-!CQWmRKXwZfqLgK6-py?bF22_y(SA@3*dZyDf1Z+fSXu;TYU# zqh0OBNyJStd8Ga!FaBcV#9wdN_};gDd%SYcAU(!c3pDL(ctL;L2a!*CVGRynT=pG} zZ^d@x*XFgk>rB{*J6?BqX3P0;*Z5lIo=4WlkU;Ry?97_;(;~-WbDn0p%rpkN z*CcQg_%LpsOVuG~jxVME^y8r5c;e$~SsV1^UW2o#im{V3yP3q@b%w~fuW{Ir(4KI0 zIGgjTvKBS0id%FFA8KD;58S=F3cDpXYh)WpQI2oRNm+HD!SV-#-q~jlZX#KWrSl`jHm|e|+P>_(kJO z%?{`1l7 zTMl=o2jZtP2tSqIw+Hjuq#~O?@5^8$y3}e_yWpl?oevg2{C&yzFCeDq)fsB{Q0Alf zC&O>oJMH_4b%zg0@e6t_c=pXZQ);^|Lyye6g*N;B^Ph~(%aAe~Z$lO_a#lurGJ7<5 zY_S!pOjLKrYa2I;kIAdk)b8hr3)V^Bzcz+8cCbFU;T=Pum-C^QkHNFO3e8Lg&W=~b6`R=BD&Jn6W(k7N_%UV-(`{fJL=kaZ=Qjw=txRA(WBmYlpv#ILa_3Bo0cX zjN;3LcADY6&%;mJA@I}Naf!Yp8`U6**>JMN8@Kl~XF)|4RL36X!`Gk-oH}bjr(UfVKF>PLgjl%Q*X~MR z-wR9CvhI2z(e1t6QMqQ|)-N zv)6DW$R=bC=)5Zq{`3~9&p;gmr;+9YP<5xWKdn0lSJ4oH8*m;M=IWLzwLVZZbb zQT_p6`Yd01MIYPf8+GV?lYZSu9)XMK#w5?nzC8aU{i02Jo@!z^NuKKJsWNYg3)Nj} zj0*|rEh1(#zTmjB!?k6rTiJWzlTdt$CE=G)V#v1!2W5QwZC|Xv4(az+UmZ=pI`9?h z?Kkm?{pFi|W!ov6c{nI>p|%WOsvvQOwhX@JqV>REMxVkbr+suG_-&&+uLH{nA{OFLdqR|LNP4AZ7aE zl}P@qTZ9(Q%xEV@qtL=;Umb!+z(SAB^AdH)yZB1oy|^q)WP6?I zGs^ukvBbZ9*H@C**Ngnu?@K;sdEz1m@E2bXa~ZD*_`MRp6|W|F<6g19d1n~ylzSOG z@1vZDpBJSLKhE<~v2V~Uor_1$Idh76y;&RFS+u&~j0w7a3O`ondsjUrK9@Sd*-z+W z)xi(vuRiFUzj&>4UI6|WQ;sArxMl6;d2`?+G-#Ie(MBI1wU|8;JFJKp=Pu2QbIylV zj|o0R)^Gp*z;AQkw?CM*G?ukAjldf8Ch_x>^0frNo?9L z{C{XJ2W>USCsH&z=Dw?7x&Q2#9V z`(+K5^YUihZ{2Zxo7Ptw4o7odusvDT4&{CW`>q?Fom%@I{Qe$GQ0*z!+)(7?H!{vs zL#pxzU$ry;DF2QI1=R|S>+e}OsR4X+3^AoqMphm5$r_bJ`q%%9m`}lXWNeKn(R~3r z@C7LUzuv%iNe69^bT3Of-WBJ;KI5O1iJir=qRY~(Ta4HA*q*G4sH=|Zk+yH4z58i* zVjO#F;0kQgI?nb*;^%pie~py?|#khWIz46+vdgG8T4Ih3s^bP?( zNwcayr2~^8uM`W$HpWDK^DN5N{|HWB{Dz^@CF-|J44%SGI(fWSAzQSHTyw89Lp6teySR z-!q`BEZ;vHcyPx6m}~nB&*1_4wc3tNRZVfZ5?dZSZ+vw4VbuCrU%|^B!}@!D<5Ntm z0~wzZ`UL%5e<$C)?TOKGZy0qh;oHBEwF*ru{BJKVhVh+!2EH%{H>UrZcwyYXB&L~s zll&dNw*LM9fUm~ux(3p{VrW9|%A1+XKW0wf#N57-Ier7O@$1nWUkA;*wj3Iu%gB%a z)$4PKxLd@W{I30{|7?-$15HZ&+&Qxx(WcP$Y?alP?VW34V7F#dn+TTYi|$D!%5l z)!uQ2ruGipi*zKr(6@ViHHpp3r(=!4F#sAGh(0O^nu#sZehs~XSNGnZX6nAw^!T=F z7h|Z)+ht6v>4)uYiId>O@44%o&(h)l+q*&b8zx68d>_n{@x7>D z-J#T51Ds{;sEUCvqyETs7Tj+QD{n|z9e1kH~7pb#_@2MB5^LKn#&@x-UdKLAV>^AEkSkv9y*Kn`l zKApP>|9}D975?Ij=R2&1Cx_$H89Ok1PF0ac&BI?T?`H8qWmn5Wps8x#qw(21JGiR{ zi+(LW#RkrIO`q>xU1>-14DfO~zIb!b`Ss{{-?0AkDZk;H#yLIfF=Y&8@_8Z z(N~>vxQl&Oe;lqSjzblDowM*lmC*5Y>J+<*&B%L1m*U0;G3|<)`e6O0n!dt!YmnO{ zGXCOEcwl>rU8;aO@JV&vo<(dC&fGo&8Z{jnl}kHwXs-)@!f)#5u)XJYL>}ec-?N9< zQ^+|Dk(X^wGA=tn8!D5H`20k7aePhwW}~UBT3~NT2rXhRRawIph~1sU9I^)lSWKn$ z`38wQ)ML+7$C5JDFYFez7e8SEDni||#iFg@xoS(qROG$U$bA#I_aVQO7?fr$aZDx8 zCS*D7@3PkJ4|f`Gur8zp zI)B~y?DXz3d?6GPldFSvOJ0qC56K#sh3%g&eQKcdsiB(L<1pfTXmdcf_(ilCwO!E6 zT~jA^;lp=WMKFa?@Ijl?u>tu{)0&HNt5{y6QJ z_rDVF(pYQx;9K2KygWzo%>PDt?&H6#Ezbyi(AzHkihh>_)Sl$IigN5+uW`v5dh%)L zgh%)q>`^Zgchdz72h+aai2QU_VGaCm)PYsz8u90QdZp@aKVhsrx!-uM+$3US3h(r1 zDR<@fm)m<3nLPPV&^FP*8>H_g&TfgxZV4to|2*rt^M5UO6W^cBnO@#>O>TMdkv$gm zwTiGGvk~Je%QEiB3zoodRK!ysq+~u9*&A$%LsF-b@Wq@M5KK%CNn=U?B;(Ks^-f-7@PT&JZGzBnSZ~YlT&`* z(Xp0mj{e4S^yp;E`ybdcyN*t@+z>l@AH}NPqaRMl4Aj$2 zuzW-ek=H-W%KYZ3nuw2%rdX~eo!mdd57ezkk}cbhZntbdGRm?Y80|TdV%Y;6P9;=L z+jBI{^1Gn0n@fOc3I9r-TpO|d$wwl_eK0O_`;l>$pOE&K{7**yaN@|bmXagsdOE#K z(tSd_ne9g|xBMQNfgQNpD5E2>YMP>)V*V$6ki?iJSv2xHNOQ^+}=QY1ylMai!|DJ={spG8c)5cp{6I!MXag4Wqb#2hD=ZO(uccc)PrDa+m_qN!;T~X-#el{u1Ix8?> z!8f^i<*mmiX12x7EU_BFpMgGo{ccXV@JlCn z?)uPG9!}pP;f1#zO|m?FbiCy!N5@%=?=C>F4xPUre~Ik#*(>h; zm6UVO_tVtJSqDUiciY6XdfWl%*9Gx-TYpa+Wy*PlD;rz5CD3>HE#3Y>1vsIZbr$ho zk4?tmLUa|3lO79ZHfctKpL4%_@TnQd26Kt!X5cE-Dqjj9R$EvLcnvOpiah>_)#~sY z$XA}^dWU!y_Y@6UYCO7iLBV3<(vOd9Td=bzXlcsPZ42Hm3S4^I2P2l&evq>CAIHWl zjr<^a=^ICOF8E`>u$$irh`#w@Aq9_@Gwg4U!nZz%(^LEmYT)_GppLavOC&LhJ+$zQa5UKV!i6IPlAbSGt_% zbgn?!w3F*~t~Y4s$Hzuw{*gBRk#%k6lRqsyK-qs_kGBK*`bK-5#lB`o=7@36W*&mK z`ZK)MAuT-Z5b+4)3`!@mu|&?FIDrTHk*LSR_r*W|qLvW(*E@QGj&8GTBX&dGu#=hX zlzDtul6Cn1smxe2X}EQbX{0xH8+5D6HryJJQ)Imz*t+qR3(uhUSK8YDy)%6;{ICW; zeCc!Kefviph8KRjzYNR|N0CcDh+aj`kI1v*fyhOUE7W+CQ{=^r#i-xr>v0^2*P<2J ztTQwHXO!4a6W>PU%nIKB8`hOnc6!60OLs8S7=@V{jhuZZA=_F~}xQ`@VN#yF@+j2>;s=>}-;lohIT7bp*wFCgL;p z%xmg!5A?f=J$?V!AUn`IHT>D(89v0B5d^M|oRnfMG{$sx1wXT~0bP0$GLz|izm@N= zn^LSNiS4$7_iabEt&+KeJb6$mb2@}MJp-JnvZYu%)~$^{MH^Bd3`uB+@0yqZUosn< zTY~({>0-{0G-R|Qr&2DP_0V6Dk)z`XrLU#Q>6=2d7>_AAbkl6RYN-0Dv!^Z8CpR{G zbrmN5n^TP$l1H9Hu%R1ET(fyS6{(t(*2D+zx2r&q09L4 zpIH^15J#S2v^{}#e~s)W3YgALCf;!%e%x6%%o=BMwfN3<>_T2O#CHzSp590-fQ9X( zy@s{$RrXnb7?gMB4DaHfx|KL_;CWP^VQ86&c*s5oB%9w{hrs znqBEm;vdzK|xgVt^onEK&;n!VsQ z+W+sy=LW{*U!;@q6nbRKKD$BuuFCUeJYUUTZoTT7DDR?wE8v?vTb%>w7I9pKZvBR~ zi63z%c7_rw;@Y;2`S-8cd3Ttv%*X-x4*K#*%!s4Dymjbe+%d$p(4x!Au|*X6xONq1 z6&YWxwDc7j<2K~nGWKp{fgRA>7RFZSC-GQx960wMTscMmgr0yy@MhRJK?4$rJuh`A;rN^x!_lGSf_s|Dl`&$N zD1#WNml@m!hb+%Df@bz38$!9v6VttrCpU*dve9Vyn`AYkxSwq<+lwC{P9h70C z?#ICoe;EV+JERWcsrmn>koPLecnRJug*mvL=RC^U5}jiG2lc!zbDDkzrlnZlW^PA@ zjI>H$wU|-XV8*fv8m>&qI&bv`<6xrCHT>TPpXKJhnmclJ;tHwm6U^h2T;mv{hpG22 z>Ux2CzvP=2Zey&EQ>N7UC3U7y=V9s`?W@xsGupbdAIwUecIyf1UPJn2wBrlP@x!3J zAB<`pBdqs0Mp{RkMq3w|##--58*O#d$D(V-SVguM88TL{zki;EIX3HETr;?g@hR5f zTz2YOOq&D2S=oO&9X2a6nT!&pn%8hSWIjP}WXvztw$H%1j^vB@sc{N_>dZ)vbY^JO zF@v>H%9rt|V;#JWd8ha$b&EV))?KG9$+`oY(BD7WYwzGMg0bBluH?z2s`HIs@~Qv91D^5^tjV6ER@(k*hZ0 zW4Ot7iFBMJ;2hj{&pQaeS_WSG&pBh0m=*v`z4%GHY10VSG;awxj)I%h83Vy>_>}x5=y8P4mc2m+S0wl+X-rqw#E$}R zEV|k77QJwyLC2))9Q$#a`f7 z;YZ>R?TJoU`sWPKVV5&Q=F&s#0onzo!|c`r+*{D+44Rx`ogOm6I%0CNbw72k5!wZg z%z=)Vu|IH=E)d*J=9 z6JDv_5F2VTtA*z9_u?H}h9ta-Z-NfvhK(W9)Sx8`(mZA8_jA#M6$Nt9ZfAhqT7~>a zV;w%lc;&cEX-Vh??27r#nQ}e8AL|@4@`4<7)k{M2ryn6ED>~~u{<||B9s{~zCu!Eu zZ&^2n^M5|iZ!-tS(jT$8>C073B5US6{I051E7rOA{>U2k_1M$(Eec7k-+z-H3+yfM zYPa#Gt{SVeA~D=+r-trl{F)eN#o`6SJ=KGp6|KNRV7CdlD3cnuhFE?B|1oLSR|Tkv zlc>9zyu}NWJ!^bVEIPw*aJ7NH+OBV(Y9lU@z<~Yt z!utKg_UgC-jLacwy$&1F&qMAdypiCOiTkUhlYgxv(W~?SU)W1vhuEC~TqS=4cqY73 z@5&3$2>H(k$$BhjeFpmS5?furopDUGvuQs#>SmqS_lQwu&ya=L4cL#4yC%46VyW;P zT1?re%-I9rkLf{eFt#LR<-9K;&+Moe&zyx0Z2sV%xrArh1}-_l6T1&r%7KGTYnkTa zvI7Sv*M}D7GjvUCGm3owihciNu8aL&1M7~%*fntpZD6loc8K?SY_QG1FqL-5`4!>K zr5$r=!<>bwo}zy3m=5nI?I`xO<4^Ev{pqFOj$DV+evPwj8oZYQ9_+jEubjv}lER!B z0noJ+=)=ia%_m1lVShE;Iukof@p04*k7S8-c3UiJx7bXH-Xo0h4Clgb^qo*HXx|`_ zw}@;+;w68DZRJ^`rLo5t-gqe7*?kE8_^+~@O;2Diw3IkZ3wZwv@5orkEt#U4Ud>WX zA9AfHmeJ#rolWzJb1Z&vv^ceAn`Im%GB4h{Mqa;SxWYzRUxa$6kraWJgPUyjxwCyuvWaF#Ioj#@9TK;#E zeu=Lg^RCSBNS-^AofWqyI5Tb??8?}z#g?rG4}vF+w2px9kaA}6d>hZ(q45LvtYPd| zQ~Y~X==b9fCN7eG#67!dEx=G>8nUi9=Bg$Kd>H#aJw6z7zI&owRSZv271_OphU{LgA(hx>*}&&2 z%93?DoA(7grzX!%O{H9!LvJ#N9F%JsYW5(Ph)Shg@i}CsT=weSA(R_SxdD_bYh)

`L;Zj9OQKmWVyW2~7ghpC_LF-^!Q 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%0DKLqSf7IUsUvZTy2Q? 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

$yZyWWoyZdY#IHVE& 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

C^-L@=@t`J_xRB?bzLhT4 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

CtMO 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%0D7Ed;NV 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%0Actp#S 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

~E=_1T1CI&||a;@_U8kefx2ibr* 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

Hox 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 literal 0 HcmV?d00001 diff --git a/Tests/LottieMetalTest/skia/simulator/libskia.framework/Info.plist b/Tests/LottieMetalTest/skia/simulator/libskia.framework/Info.plist new file mode 100644 index 0000000000000000000000000000000000000000..ac23c3e15d0f5ea176cec4ab46168d1312d3c5bd GIT binary patch literal 756 zcmZWl%Wl&^6rCAf;ZZVaXd#922oxwF%abTPR!E5nib~oLI}M)VqsEFVf z`Voaa3%-D5zXBGB1rlP(nz6Adg4x`ebI(0<&l&lgg^?_O#tRS~I(+2lvBL2aCr{0t zF5=RB`OL!N*>jbp^B0ycUbxM(0E1IgTCAXoXJu^Lv zzDSW~%($iFO3qd24&|QjP&evRn|ZE|i+OL824RYn)2XguPMAwK)bzEjXgzL3V=r)$ zUvyl~$9jf2HEgQQe1R_Is5~)vg3|Dg_)69#Rz79 zmYUD|k6uwTj*JmcEVewyXPgE)#$DzzCw4Z}|3^BO@XN-G2HS&}1 zRc5A-8IwUvdM#aTQcq{xu7Q8;CWNQLJPl&~YXnUS5n8|K$Z`{NXBkiF$8fi$iO;T?WSq25r08Q`&Xg~+A!7g|Q zK7f7j6(U%L_h1ta;7d4zyKoP_hacfS`~<(iZ}2<(0e_+bnnNX2Mk{C)t)o7&kc}9c TA|J&dJqZ4;;7(Svg5LZBTE6EC literal 0 HcmV?d00001 diff --git a/Tests/LottieMetalTest/skia/simulator/libskia.framework/libskia b/Tests/LottieMetalTest/skia/simulator/libskia.framework/libskia new file mode 100755 index 0000000000000000000000000000000000000000..a3688204a910c7fa69546ed0df7b3b6a4b31a5fa GIT binary patch literal 2961400 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

I~}@*s{*BlLhu>U`Ka`%@^23onfyL*jv@8 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�v)5Phef+pY)gI?6?_PYB zc&6=v|7V(szq^yTGv1~8jQrQ9)s?9=?G-%Zd9s6eUar%YvF3;Y0?xao-ga> z&jkO>V0+%)-Wl!hk|(8{*oZ5F^2V8Kd)lU{p5uHk?H?Pa$XBFhwq+7eKpYIV!0r=Q zkaP7;Xht6GW)fo%OS{`Wft)zB;}m`c?&+%C$a%IEJZX5nXVWo$B4bNm+W9^oczr}& z_p_G~8;g+}>OS~msU@Lv3HU1Ot4#J_8s~<8V$`KU%XQVymp+EBw?C63`$T89QP*o% zW2d9N{ zFR>0||K~_z1xK+K9IA4%_Gm3cayk}1S)$+3cd*AyoeYImV@Yrq8F3nAh z$hfY%u(N#~e!DGGLxw0@^c?k=>$0xmj;=^>m_~+Z7 zw9cu&EZ4|)BkfD>=Yh_p;HRrc>iM z3xYnKDz!8$OYwB~PO~<2W!f4#t{B#Ea+)HiZvf}tgFgHj-}^(8thFNhNIZhbPtepe zaux~llcVR+n}Pcx;C~bI@FijsZfsy)jM$gKvUNarRXVW`QN(S2wRsNm1pXnla>lMQ zcw?gf+G<;EpPs=Q78+>k3DE3r*3)jSD#wVQF!d0T@g=^`3(o3>?h@}8|Ajm+&&X}i zHSLfEn1su<`f!ynxR1g3vzF1&aL$^-*Yg&#-~;%-WKHg6eJE(S_{Rh$a$m%Hse7uh ztFCXod}`o)df0jqTTl@;@%!SyDWlL!5?LDpT{eppC=&moR-KoZZaPx?aTX0LRPUc^(7mnqy z7`##F#K-U#Wedc*!b>-*<#|12`?T}P%)d^&h`wCrKiRO8&!p})_|CH#_`c-bl>T?J zuFJ@eVLZRI|GD_?=kcA;;PG7lAMV~gKC0?m_}_bGlI+QZkShrZBr`;GCNZGgqohm{ zFS+Qbfa0|Tur(n9#Y+VNlL$g!s2YvNO3#l3s%E0`h>DU{N+Q*AsaB=+9NYJl3DG(s zq9B3^67%~$duCvW!P4XVp3m?7{jq27S$pkudDgR@^{nT%2ENy6Be*L#%N}#_N}-pp zICIj6qoW4D^!yKh&50n-5!^-Lbr<~^n%0d_XL)s9qyQVt@5R%KCt^+R_7M0pA}|)()9BBLih2U-=I=pN@tgHlq7jgOYvPS=~vQ)~lN_;?}Ijr|T1iwD7G1Vy&2avLcLz4%V zm3(WS$9{?y4nIW;@qg{EMz-Ong~_@&i>$d2nX>FH;ZvuJZe8|hBI7IME-^35 zxJPlX;VygjChi*dUEGBy@o^WPNuwO0{dVp(#7kcFGGd?!$&7isxhXItAU=E z@t%EQ@7k2A)@j+w;x}0X*0FvUcf{5fEiLaMrt{-=8@wj`{Mqcz{Oh3g&suU-YdU2d zfZneaJ9o;kFD>gakEm)@R+u*g^U1oii99-=(2-cHd{o*Kd>33B&8&xxEzTUrMCH0^ z_|Zt2j*;wVrEQIR+qRj*47zWY6`7&LoIJV6h)n1Ed|vdu-6@}PAA6lf{ItZDSnRAs z&aNTj&%s_?UzE=y=XDr{cUK{=@I6S3tNx*rfNxGiobwIk*Vnku#y9q9ci8ENA5suOf37zFO>cU)=>?)&=iHd=s}X z1z8&Xw1FQ!VqwW%#*M5?Y%zsj6<^%rul3a@7bvx2e<^vy22*7JBKN9c9F>RiJR)CL z_OKsEUOia(R_r29m5=|5lrQ*e#!l8ZRO#)iFgawODRusp`b6&Cm4RFf`H%Jor6;s* zkrVuux}Rh(Xs4WA2K%<3gC`n1Yw#SrJn%{O|D$&I)!)b^xGHTe3)#B}ZUuZJ&e(qm zKQfqq$H_$nr~Hi`Si}CTz{p-clKp{&eOVNG2qXJ4>`xll8)9e0-bt66S0l3&zPNAM zRpc#UFHuZ6MxC$IWnL@%^7K6H7DXO)EJI?A%*cj3 zjP&*fL!I1TOYWu;OarjP#ClF3ygK~z|R{jpE0?ev}j4AKUWM<>ZFb$$}snB)XoMwf&Ym+H1=g? zU^;Ld!kT-C0UBzrFLUiC_G3$_Pw>WWv~Rl+y-)Tly1jR^$T#Ma-U-g%&V4d?*lPht zXw!iLrM8pz;x{e!>jrFNycT#%$`ZT_$|Gct|In_!`rmSGMKwzx9&`jH-PDm4=6r?N`FksoKX*~Cf3G;K z`9a#Yr1xZf8GCNY>j-l`mLzi-U!gMe0oAhaqMhOobUQGO7n^9>SQ9uSQe+jf*Sm!_ z)^hp#B(lKM_}MeN2%1)S3Qv=s$@7fw%ikyah|sdynAe9H|Lv>AA4X(b8aR%B>as}u zVhptt(3NO*e}6srH^}@*JkQzNaQ{6$R$#C13*asFeF1*H)$Bdx9k})?@=hI2;vZ8s z2ATGkEB0yFd+YKR^36e>Gj|m03}b3!J?vXnOx;D`0`1;us4Z2}KP+AC^lNoo0bsM^x8($I1s zzACrBQ_YdO^|n(l`*}Uy)nMhy8WC!zCH+@uuODw{dr03K8JAM<;|#k+=2GbYf&2MT z_?n^Nq3~_car1N?ZQ!H7|EKk^(sv!+?CpbnGQv|chk?`Sd;-g}^@VcJ7)R=zAp6zO zc9#z5*Z1{f?SOuGb(!DkexQe2Ec9L4t?(|`ZUcN@Xq%lf{>%{@wpv1y-hU=I{-^sD zT>sB$jHvfq#%M4ZZz6of^~lwNIVcJSPHkVOw6x)K=v}#!wBM8V)>L1Gvvt>8rL{fM(9#xNm0LwQYO44k#y8&w zoU8dS>+coxC-mLVcsH1=dN7!2z^pI?W=3Gv0nApi9tqrnFnj0K|2deY{2zi@W(drB z=}#!kGR^`syTnZj=|#uXbn*I%;U6KQ7Y1|L4ND_QHz@J-PICY-^z7 z2#H^{UvRlck9~%`u?s)za{R0{cx0o?$+me2uk4c-g=Od&G)C!hnb zBa=^NJ(7LHR_N#s;%taazJYgDr(}#hXO+pzS}HohdzllW`|gHbAUkc{A6{7D;kh9k zJtaQR5~o1)l(JsWJaMvK*8iS>uBeH&1@&vP{yqKBnR=q#lTCG6|Cu^F6bz?ycA}d! z(Eb}@(JiBE|Lf|FPp6^>)$1m1y2!%rjq&fD?39zPrlKF+@bpt5WrXUQ@Butxg-?_< z-i=;H+Uf+}K49XPZ6)dZIf;c)lhKB)L1Luom(Kg!@Rtwj&+x@MrF)wdqI;9FC2j$_ z{{-4mAw0@F;1SfVd5I--8eWUi%)AqMo{YbYThe#*YRS#60KDwI5tFKD;|k#QHhciQ z!SpWpnEs5P;_A5_>^-Pxji2nsXP%~?2lY+lp8+O|IIvujkw$XoCL*x$sGSN<(^*tm>X@+rTxBaVAY~r{ud4ScHkN;@h^kxlMXZZfS_(-BJJA>|21c|vqxa-mkImrpqRr)^4JH2uD!CZ zvKN2J`pKJmdgdNx{V zO>gwalNCMKi8}_@gNZM$=%qz2Ejmfb^0C;U!uu9 zK4MA#pVXZGMY^0K$G4Ms$kdH4EvE?_^4+Jj3Xk_6^z-;?=eGSn_R9^9r>i+VJC$0_ z^qV*YJmI{jmyzw%{$r)7PUed*BJX9pQF9;PZ1i|jSwq=JNGv3=FMnti{v{QYrf>#D zt+ep~<#ZNEtgD1(_Xb0qFTFiy7dF(s3xF-WZ~q1CJJ@@83nbQrUq>W+vrKFrWZ&V0 zcmCw}#=0ch;U#v(4%#5!7UJ(;xmxM@in$?sa;eLvR88-BnmTs0>Jb5y_IkEPZrLZpuuRYGV34J*M+$6?qgVFf@pPw?+?H|V(V-uB`GG8>? zhbiVq#gENAe~!>LYqkE|2#r7W-ua-OW}Scz0ojw#INwUOwxyYU&3ND;@-WuOc-Bb# z9$H1;%ltU2-WL6Eh4`NA0ERkj%F;PQ&M*H8(gugonG>ldkDCS@e5LL-)i0xC-2{IXd^MAGTQkv9-pv^|(uRM3)KFJu7%`6aE{>0VJSAS$ zCRL%=lX_I@xjNCv`{AeF%Qr{#Z&FXCeT8gFzDcZ=vQqSPjk1W-(kkb>_E$!E`YSj) zr9aZsKbw1{+0#FR`y@zeMZ z#nV$^@*K-@_KJLQ;u@u=_Zp>F{!1R|mz>#C=W@80U1N8P|4*s$))VNSX3F~-!-l6d z;AkEKeMWvIzQ00qhjZD{(QC-?G-P-pqwqmLk9HS(ij47lgKJ9h^`Cs{SW&zovoHVg z8~TdeQ)cCP(5*f$G|PdUZ}QTzZNAw~&ZgD6^TD?pXDOapw-_9SGOun?dV4Z-SyF<1 zC-S%x>5~NKHf0#!*W-{eA5(h!>w7m}3P$+%Q6J5EceAE-v*wjt-|^)%?EMNy3Qqgu(FEJAu@*)WKVdfZRz=lopFVh0dAYgS`=!{Z zIWBfT?c@7GzVAv`dOi5Nh|Qdx^O5ag_(Wr)^xFrKM+5Jp^N@1_57Di6quYNT-TuTC zmJJ@-aPU*(%X|Llr45nYcFqD1wn5|l8FhcEygdJ>{^!Z+jBpVDMSPkoiRYrS)|{9{oCL~Qr$$P=n%b_-k)DI#oc)cmK?zC4RU}0xmKhnSCqvMaC*y z1qPAe$|<;ivEpTcxu%?aY%+a@UwhT}?TUT7hH@UPtdrEQMGbRH-zMU}IF0^Qfk%Sh znUBogmK6^T`9=CRORSP3@8-~_Uh0mFN^<)yFnbO%r#=c-w&sEt0+Ye|`q!TRt$*OX z4$}d3wZpGSU7_XOWKy;|g87j*H!%j@h=LY$3N2mWqd68iEbW!{c%irQy>;b(O|>x| znJa1YW_>)i*%%LN^}kJZG!nl!FedHHzh2fUp^)9C}3Ts>`z&DCvh&W*zCx_~Llcw1YJ_^c?z)e+h#v|0Fk z(f1_I0p_=GJ|=XzXTOpodnK{4^>r_QIbCR$obNf*`SLLATxB2MLLYY-@?P%QZ18xY zTL$uTV@nY*GZ9YIbokrnn5}U7wNAT@$%dx|oOH8FQVi5L2mwXXQ@9_fY z9+&Ln8w=F;MOJwf9}aH>{08r3+*;l=)~%&ppGD_cj?@@xo;KT>D&K$ z^1D1ws6Ehfz8~D)RL&Up+hE)q&e+oD%jYO{h43p~)UktDccavr6iJsjlzLoMfd&6( z(YHZuG>wDVwSKprEb1wv9y8Zq^*93c{VVyUNwIvi)IXK0|FjI-6VQ~G0Ko`u09 zhQlMOwLyDRnJ?I^KC0#U{nuGn#K-ATZI1uHJz1I50I#mDa&9xQzWlj~^U|=N{5g3( ze2_fwdqID%_24^xSoo4ms2zUtWPJlPO{L9zH+>f0$(o=gY)rvc`cX&j)vefhFSej} zp_I#KHS zr`;;{ZFx%V5avK7KF!aQR!-XN6{?&e)>|}0(f8e%W~DchbfG0>#FLP6B0b@~h5To} zzdCRZ2~83Iwjzu8@12e}(7#jF>09tUHn3;-N(bS%;hU57niaYApPiVr~#%BxVN}hpj7W$#r-8P}Nk?}i_q10X_`yyz^ zxzK=r)}K4hQm@dP2I#_Fv|aW`4afcVqvnwPgnIl;8`8nx8EP*agkdpt4whHP;rt0Y z4W3?wU4oBz(u~)x2UL&HBQ7 z)l&*?2;SdM%&0QjvSXC$DP+DS(iSDc3Fa}uD7 z!cRy)=`Xgb?E5FHBb|>452u)%9-)bGjC~Jnk+@Bzn^Z@cQTAu_U8+OWyN;KkxVR{@w?b*16!Ez(eF1pFi7Ie}KM}B7cR>>avZlkTj9G zRFm)HkhChsMbh?@Cf^Cnzt5&*pe`sBUdjYNPg#12(mNMCD9sG~AEWgCSpKuW7v2S2 z=)GC~GscySx1>FvZmhdr(!h;M@IlhPXOO$_tOMnMiT_#P*!172>wo6IQGfqRBeE(DzTOOf9|@mtfe*o+ zW7!Z-UxBgq8|Kp2%%vV+AwEAc=S60y-C+mAD?*!%g?mq2H)PeoJ?33=HKfZkd9|Y1iUu>!? z9H5uzrA`O3@nlK++E`cwpP%qu4LEAHz z+Iz$1ONY_!gM;c*tk!`i$L;#2lA1c7;bzVjY#a zF`T(AIF^D>tjP2VdEO0ubd=w}jkZ@bvX;|B-#_-|?{#ym*!Kcl@&Xgytdtg6kaPV8m2v}c4 zZ#G~rZ4()*j8C8+hk(DVA7Y1fa!~|$IJhmAoUJD3>jnVNsd^H7hx*cPMb|yEE2KPh#w`N{ssuNu-#l{It#9y_C zI#1C~~YiyUtMkY-32jyU62{aX&>bFWGjGG@*T3D7_58#Jj=| z6P;d?HWZkI(o6Z~6usoVhq8rd?iw1Pk@NgA3gK6D`Z#oO`uO|b_0^x?l5r{gKj4ek zG4}iD<9F@YPA3ID8ba4*8n6i%w@CmP?ulYdmXm|)s?9yrC(o-~%GYD$ajS1Z+pouxaccUR$ z=e-*4U-I9gi8n_olXo$0WkQRfi3bBT5q;JvnwWo%G_eDk=nJ8VN<`K4=H-5Ur_>?q zr*F365&8&Uj_faywKJi4w-MV^*73pC&Rp=~ZZ6rcD?+yi(3v>{_F$ra^HF|-;8Q?8 z*hbyEgjNkgFGrK_mO%M~)jyJVGq|Mvq42SBzF&}DmO?va?3Pf+V0(pNymX!Zt(4dZ z_~#eM+QB-lFkj%k1STSnk~kfAh`ne~J`uu8G_t<+Zug&+*S}J!&Hh~t5s4Q`6QT>Pc+2^$nQ{pzxyS@qw<&8NXlp;t`-+zhVQ zr6IWa<~ib~jWu4@>l}T(&ep<&{iMMo z@pC59j(a0|UKiU2(G473W~x(=Z)-}TTSHzh{D#<@m{%!1b<`!c4ppq@Ybn1RekD`q zQSF>thAxBg{+h82;;5HAzO&%ya`L^*B{(q{j^4w&W+{s@Lf7;T!P(ILvm*Kp zd>A|W`yaL8*Le8rzIr`vy|q>~DKFW)V{5yEXHe?#l8EncMobL9e8WHYL_R2U4?mW3mI?az-+sGL%GT%Jcj2To2j@4o6U}DydYSl0 z3%}U_4;$n+--LdBAHQjZ-_&I+&;=WGs0{d;ZI^p0MW%wx1D;#=!Ig7JLiuhpx=1rN z_zCE7Wsg$GSRy|pZf$~FWJL$h=o z3GjiTb$uV~610G?G4c;-{}1*xMph&;K1JXl_NIgE(>A@)SO1?}rNG!KYsJ6Nmmu8U zVeUYWPto{~nJ-<8zrsApL~bd(To!bqM9tcE5ZoIzIu9E*!`=+WPw=pT7;9&n=l9dL zm$=TL`R9P!H~qlzITsvo{=hm0n?L`jaG>)3v*Umb_|E#@j06A5_zxw<;@Q^z6Tdw@ z_G8`{u=bw=9&moZ>9YR?+z!subHRh({lGf@C-IDlpMBe4Dp*Z)ph@UP&(KLT_> zr$YmHz$GDg@WOA;!~RqEHv1Z(>*rebW5Kfd{+>*w_hBguKQz6a$kD&ItajSD`0xI9s<-u22Fv36 zd(xENWl|RFj9!+mXZfCX4Jhl*U|D>B59?|f|3lZf;JOgX=LGF$&DMVwi$9_HF5yey zZ-V-@)`rviwROZcD?(rPJo4fpsyT8k>smK4$VAud*Q>45^=iKe=+#6&_5yqT7IbUb z=++jn*G^acdbI_z=Am1QHaUCWMSjRRSKS@R(bf^~Eq=YyV=vDn=GbYSgI_j(`oH9} zs*&zs-Wv2%BHySnoOxc(!1*1FOBSj_tP1I)#HkY9;gEpt&`mopWc?H$&HVx0A!l7! z&=X#V{xJ5%K>tK{D1GZd4s|tp^sV4_3*T~HMt2@*e*IfQ^Ao_g>HIuTI{Lx+*o=r? zeEx$N>AB$9myW6}#;#5Y?X=Q73_nLobA!@8a9d3>KQ-zR4~5Zh_OwzwpBg6v(G z??F6{2pwbanDQ1|7#DqkaWvj=;> z0FODhO~+$1^}8i@A?+?K&)W7E=+s1LoWv@W*oO2)k8NnAk6NBTuA#Y%IJdMvz04oi zaG;KLKI1wtZQ!0JIL4v)y=$X?4x6zZnNQyf>3cV_lPu1QFQf14>BFJGJgx-CKi|17 zg)_Y@=*Cakj4WghM`JTG2Ah#-YIH?Auv-Z3M}zC{(s!A|8=1o@>E(=(gKx_jqb_Xl zMBkCcfAD*;{J#@gUQRn4s>PB;xt8*ZZQaC`yOT80$*<>}`e|xZMb-Po`q?uZIpvWBj?fVr9bJM2khqVKa(cD*#Yklc+h$-cH34stTlQzDo*JGv@+|wp+lM(l9rFVIyzu6Ae^%<+cy~7b z{IOQ#L|k5E;^#yskcXa2zR>t^!Ps!K(Q^Pp_EuFNbQukq>XNcItX6B!~tSA2wtbFh6>q_*>{a{EA6x25M0$*1G*|pUv^Y$Zb#pbA6LuD-_TY(u+LCd z_J%UWmX^mgiZmtJmS!W(Hih${HCvj~ZcUT#t%`HI+y|rU0%wtl;z!;*=Y{pt^A^5% zm&myGAgfTToGlvh(hZ!4T9$%-JX~ohjjYO*u@Ie(Kb}Cm?#EOEp45o!Ppg?;Hdk3% zxkt&-ygR0sf8flS#@y)u4(8ipW<=&{GqjOP++O_NKO0sxeF1V*ZN-pOyUmhXXp2fU zui(8cIZn$nXR>e3)JAIK%qwDMWSUdsN>&(WWQ`0TUrt=sNb;4Cb|-09k+x2?WLg6M zqxgT-$mH=gq*0DV$|xIiIu>Sdy~hW+HP}8Y=cWAvJhv~8nPH}!Ov;U(2RWQl)WT&86M!imUMd{UBiyD9Nr5%hV2@-wgxv=s>FWIOW{k2Yic~rpx)~)o(jsb8tm+>97dk zizfJDv!*FAW74c)+_{{{;bvg_c=v-B*Oqc-^xR12_L;!1>Ni*PD%Q+2tI0tgdm1<} z^IZC)7^E_ zGensvG{XOVVly$5by{)N&~NM8pf$42)I8u^I&8FgMpCwAhTzC;y!(W7c@{f6u}|y7 zrtxEZ?CP1Cb?d;M~@Z0`;V>qy7ke z^v>$38a}@0ty`<8OW>l}u!RCo51>2jgugJcHp>3Hlr!i$Ru(K!tc}q!rwv0iSN;UA zE8pH2T}X@`yGfZ`NW328%99PIWLN%(23Mi-)Bi{7o4v2lsBAs(2jf`V?VF;x54GL7 zDcZ)F>~`{(MJr3~d_MpbYL(|TUo zTks?w-yS|xo-bprUKRMiR+*AkPMbsXD3WL}5$Ci<`+1von5{pkI<7&3;PMHsn;$Uxk9{$;R(neq|dj)S;f4#xv z%*M8|&uEJJu~XV|Sw>nIbV_iKIkvbfVq=QLHxd80Jq86k6!--85N0(ov&mqO)YODb z;g6~!o%chtX67>d!x#{8q;DRDQ}9 ze3Wt27)KxDC~NfD#<7q(1ZP!v)nI<*d&W_d{EXwk^;_uNz}P@`O}E2+H5Au_b0BE% zDD{deLs`?lkfsf!povl-^4pVKkN5_ZVOi?H9@a@s&&%t@VFVf9x%<& z!s6mGiMv(I7+6*C_wcjRZS+BSZ3F-FxJw<4TqQ}=6X3yq0?stC#_qz8t^pcrp>7#3 zB~RjlsVxmcuP#vfw8PJRpc&e}^a*X6A+#whgf>0or%m`cdo$G9^X~e#-peyF(C$)L z?X(dq5vP4qEHk0o}~Vd$d9IuT3@=&`3?Cuw}wvH071X}grMi8e~isEg@? zx4>_cdwZEu7wpeo@Kj)<5|3QoXApzB={P>$LzK9NG0yD=@%JiL)wswNrtz6WV#XiD z*A)Nh?E(Xh{k+$x)b3G$5C8F_(d+A^oKNV3&{xak{(9t{69iWB{vqHc-&`tp@_j*D zv?b|rdb}#yBJW5?x9N3oCiC9st|@w^up#&RDF7r(glwFQU9tMCy9_#oqglZ#B) zScPGy5{{k9nK8uscShoi7vB60w%^B~O`REPtJqwLU$OAl#|yODhMmUuFYT4OP4EAl z{@0wJ?^rh`&k?UimRzD*N`g4n22Xqnw^G$!C%CFOZE173+qiovYaaL{<%vI~_2jqp zHFrV3MD_sPYF>H$w&JDfaT0gVvodj;&^3`QOI%pVa~v27jT76FzY!Zzi5ZoqLW_6c z2emBj>6B3;;NM2r(v0wOw{y3RfL_Hvo`N5MBP(`Y$~@Xp6uUMB`A6(%p2K6tr5%Vj zZ1*M^wl`?!r+HHi+dob>Y%is)9T}uwVA$TDr1qMbFXouGFT3bdJm-LWJ59R%cZwGvx7PE%vX`L0x3*{8SOJxaMw^4%PQQ+a|@q%{K91K6D{ z=Us!~Bk+;3KKSO_`fa?ExVjP-b5fe$`4MwBBfNG;SYQ2X(oUW`c>f{);aw*`4owpLx^)&~GEsRIoYr}y-n&h8 z9#u(@_rE00pZ9Oh-p9{J|NA%in?M;7ucwnS{L9a&leFQ;NRO_$5#N&G&HcIXM(h=X z<$XeVM`@q*wS$;?H7ELC(qhl6^}UX4*+Cf>HbUaGt?6Im3n9tZ-h%C73gBkBohu{_XQ<{w(C^(~V z-<7RQtl|8kwb95aRdb1?XOZ6d%8j12#Q$E0FU1&e8s0jtn!dk-tg3mW5?j1@r&jnWB!nJCB?kkVN(`Yj1yf*)$~R1sG zQi*cglt`{HH}cNdN^F4}(v_L}bCtyz%$?hn822jn*wYkK_a@}h=PA+dRVIAg_& zssHWe>WsIQl(?OWF>@L*2dZP0nX5FV_7&dWt|;!&O04@I#@OcD6AD`9M&-AZn(|tH zI>Om<_{u9=9<|1}uCvCvTxl=6;;eD5KUw2lu|FNL@S5|Z7CvfCa80)+y5jhMcx+p4 zJbB-i&~ zGQTC3cK#=IPa$9Lm3b|-{P(5raP3e3o$Gh$+g<;b{)TIL`fpvgr2ocs6W`_V-9E}c zKDjOTx|iB>-zWbG?El}UJ=c@|!N?a~pI^|H8)1!h{o}l-g`Z}$<*u?uxO#Xunfg7n z%rScEbPPUR8%CguI2L;Vvc zv&UEs?!LX;01l=4afp}}9pKoiD5Z83I3&2!2@VSGi@)D$@cZrMM)HsF=Z8iLPOgek zYKI9EoXgKLpBp>GHe5+q&RKo>+QNEtfPHyqMqca3@H~q|CnqvYb+kW5=|*Tj z>qFzE{r-rw`BpU=3o=i7RCoZ8UXaXquIfFHUX--pXQ zT^R-NN%`GQ#%{eD?Ur=|olC9gaK}Igvw>qf|0`6{ohXZ)*qttTfA*A?IeyyW zrLKh?v!}efLY?uhni7XDRHrHUKg?WsMA8kUr~1>ulTviL3sq+hYZd7u{ORC>=xG)b zOW@rVCelax)4>BT{a z-d$Pj09PDg#3)&*dS>&#FG5xyhx85~}Jhryf z+Ar}hlCc%IccGKnsNw_RRAvf)ap*ax=b_Of)5^Sf64)!Jw>`i*g8b23W-eqYinGpWr0bS zb4^>iyrsLo$<>_|-m=bUc4eu5o4W9cW>@E>rj{?3J6ozozTo;yjdEp&wdHo+*Ot5a zi5Fa*_f_R~&TGq^_e7Jcb6!>MnC!}yw|M??C~Jxu=jyC%%iSsODywqaDeE<^%$)p| z&MVt;3*`GN`F@6!MV=W|x!EJza$hI?Go!)Pc}<`W`kt-+b!z9bz&F5oCi(4-w%kPW zHqq7#9m`reACdQz^GH?hIwN(v<$K!irViFay^azo>%XQxOTHST;_6%!NMA&r*@67C zB~M%KOMH7!r5zQ4^a}D!3*?_ho*`|yt(4=V{?5At>35TdHUE5yJv8FiuJc&GJ1%X@ZKU3g zORI9LN&7YL-XhOG$Q!{o7xC?4%6OczUZu>xQ1)@^NTRMO)OizpO7`%!+}}#u3)*s< zY2QI`=+M}<+|1E!xi`vxc$X&NvySH|hHN1j@aZn0B`Us z@PgQ>xo!1taIeaJlYDnPvE9|Dnq6POH!R}WJF6|X^arw)f|s{NF+Cf`-nBet&onJ0(Bi>-dvb-|kFT&C5(cKu}aGp?Vk zdB(Mh=TBEZ=L%o-tSfxYv#u+7K996#c)w~DFkbbXe)obWwp9(TRok|>)*@&2GTw$& zn_OOS!?bF%%L}e|(%+wu$IDn5`PMr_-u>F;1*i4A^if^a=<))8{k~?CYtx!vyFR|A zEq5hlE z?dS-|EA;8@hny$>pqO26D{-zZ?GLo5Pd?|eKKZO`OP;gE#PcY5ZqI88<2i%pmTLS> zY|0j~eVPYc=K-5?+MEw;^LcjhEd3Ff`TH_LxA}{sAFb+e_v>nsTlzJGK3KKk?&LP? z-plTrGTeCEwhPjg)Zs?<(U)=`5ASq=k}9@9tWnql4e1t}jYB7mvHh&oEnC*4w5-tL zck~$-H*9${<()ob?0)1tEn6NwmEJ=77}BSazIzL@8}d&hzk~Ej$`}({m12%rtILIK z>Asm1&Fx~Up`g>Q$%$uqp4 z!q_!(msn*6VoDgfrgxmIKZ+hH9-owcbn?~6n-6^H%#qk~5=*p`ym7>p^A-@tj5IX@ zUYUDuICs7~$~Wi){;7?-uQNYqt>_!DspFI-=|19R<^-u zACkW+9644|k}*zBUOM`wk5x{yI11%{V6B4>PaDf^FQ==hGRIhUeioT}>>6Phoh zu76O^0c;)wwj#e0xSwFJB0k~Y(ElFz$xn~QPf>BpP2&Bva?f9{F@ zoB+}NNuG~V7}H7?0vnYMZSFV0osu~}P6d_Bu-HL0Qz8n}+{iW{@| zZ?14AU@HuaCkNpC$SF9RJYP%yZlV2mtLVLkH5Y?FGC$IdN_RE72N|mrY3G1(oT$d@ z;}=hv69eTgrQAsRC}a2yewoKY%9e9#UXK%;Oww_3w$1OCxD2}BrH&irKe~~2!7Ij1 z_M_HCraCE4#?g0o-l=)e9>d<`3&m3-V`x-*e?{Bsp^F>9v%9%J0W5g8bhsMt7Mz?x zyTsqF>l%rNld8u;C^MQ%x^5wM-3l$Wi?Kj&=N8?}H`j=bUTSlbA<3Od-r^O*Qi+w5 zd2Ktkg6)q#eYAu#RSY944gzDhF}~z8zIC@}xx4xHWQo#x6kIz2j#rT9Gh=+_V!m77 zUjOvxvxs@E4zJj|<(Db@8OH^D-#trd{T%zh6EmEx#ICHk`jwg#)-e6uA#_A1j6<4t zBQyQ_ma5hs>iHZPj14ne_9L@8M!da^#B1oFoY}Nf#(W2|I$8fJsmBXm{1W(V01g6! zuZ%IxECJ0iz~TsPJWQIpJaq%#0srC6H!U>P1=DvBr#p+W_Zh)sHO8ILezCidz7A)v zvX6byR?cshGUdFP1AO}$-zKxB)*0g6yt_`m*CZ~^7fz3p@^@YLZT$)6q0rAN?@65| zz8>~%eQ@kQvSI^hyt&6Cdq>CV_z8{lp>Np@(C_P^<1?Y>Gob6!ks(}1JU{=rjzYUq zKkwhVn7yw7xrLFoh@V98{AHh}ychY-=vujJd82EKW)O?ud13{CH(pJ7RJrz|+LrT7 zb3}H*CN)22xm(vws>-GtDNIf6%Oq& zLVGw{2U~>@+7qy6xPWxZ5c$k6DL3fbw-87x*xPayu#(SUwj{7uCdTG}r9 z%LZdE_G4AKLH$ybaYRoe{ojH8+BU|uA!0>~5}~v-8E@^;=v$-lo@@<0x#+_i-1q9w zqGJ+0zS#S9&60E1&6$P#muJp2%1n)VI1xoF@)oPu0jkCoYVQBFY%hfktV63uUtM9A8<7-(5Bl5FF7j|H1LH0cE zACGr#_ZpP#9pTFM1K0s|Ml0LB7U~GE%KdnhvR$_mIbUfN8JYA^%9cJ#+4t%5C8>G+ z{BP?!3!K|a+vwXx&rMfFS8TPVb+HZ~nBm->7f2V~yy(^)q|cj@k@j(YVfH-jytH}R z=(LZoac=))mUFxOpNq{~K53otCE58}2G8fEbmrniCuIg~-cCn{z00(5On;7e(C;PWb3>P|+ON@+G1^bf*r*cw>ac*yjQnptiud7m~aE5wHa|-=& zGXI?>ZL-WC;MgiU(S|f-due#gBxeNee6-1ro7gWXjHN*z%ekk=^4jNgdrSp9YZ#_% zcMb(MfxL?SbUt}6u@M?ll3%`Za@fl44;lN__OH)LP_De6<`11Y#Y0BKMJAfY zH-D()NV?suWon-&aZUWMv|#@O9++ow4qYJDJbQ=f<+Q!)|fr6y28;XCJ9$HZ4?iULgKB>(3yz z(B?sGp(6vf(3U}Lp_{O~)~;>hY~u^k4ut1r%etpv-=tYzi__`h66}Gj&&G+puTqwg zCU!qfZ(?iswvr=u!1A3_z8M5B6}~<74ff`*DavHAIoIpN4!vovvh)T;o4kW}HSqCk zBcTo0^M=M{?3rD_e9VVm$V2Dw2VKvwHURIxqML6t6l8CT&d-)M*uZaPiY=`X+F%p> z=Q3|<^4kDG!wxZ4Lxpbf-duP({VHf(7k(OIQ|99=@cE2;@to9J_mTdXtIEr#RG6dO zrE2{@=o|O-akUx-BK3q z-A7xc{Lr;4xNZ&R?Xl*vB2cNgOqw8c(Duk4R8CTBwo z)AuCL9__DxY83g%Uqipmw$t|JHQ4O$1|Dlkw+GV2W`8Yv{i=Yy81`kH0dv}BzXqHA z8uqVd`Y+?5V6!i}Cp&l=d{=}mvh;l>ZM}ypXn%a1nE1iCld>M2Utvn< z&ibuxlReN6r;s(j{nJE<;UvplWXU2{?%ab&HDQhwH@fI279ma-j8#au`elLozI81i2e9m zI(->v+utO5)c;Fu`~UyamwEF|2@dRj)B6vrP>nsdQrw83ObUN{<^%ul$lnu?ylJ|}wZR73Q-so~MkDC_~#$FG|j8F)_PIXb#% z@yPK9(Y4N29rJDMLGrDeqYG-h{=U-3wFYI9^tp)pz`lCm`&Hhde;9Kfc{6x6h}}Ew z4-2$Ew5<*7wH1{;Sp;j7z=Er|4ck%ddpa!nz@Xr)FbFM2#_Qyw?ZAMTPs{L4S&2-% zAntwp@0~fj;Kz7IzU=oSI=RRndqrrXc3xh2$bI=MOBc`T9&Vm)gt!LRoLA1d?=<>LpGyt2C!~x9 zI}0FNb<``@T92)q5%C;0RE5kKrA&H}s8SUAYQUeanWsNFpR zKbQ*?5Av^Li&dkg*j8K;S?-u3 zIB6&~wQ4cMl0ENohuCBmDdrN(6!cCjEUCzM+%jjh3k_%eK3ETYqh3!XwtIW1GY`J! zT+5bko=3NrNgKP2#>`5qvUo1#VHf(r`IJ)?WpGtGPp63`|b1W<&BM`8I3uipO4{ zoG5I9TA&Tps_t(v>gX17IGKct*&LEmRX4yu1o|K{@zuyG4LiNpq|ST*_K zny5^{KKh9G3k!TN124}8qxgRjM%}W<1V$#tE|M{l`sV>V_8fX$gYn5C4-}p+v<**= z$?vU1wj<+|L>nHQYig|_R>!*YPve05lG8Xa3>|p=8DewI2(CKsFg7Fatwn^5e!P+FY{1SIk_89saV#T(K`6O~) z*?Y(u@nWP>ci$*@ZRF_lpS{c>u|LhKdG9oIt@bS&Q!MDk?C1=-o?H7@&XXXAY?7jYe=T{kf273CCQl@JJ|_;s zVQ~Hn?Cs<{@e1m_iu?QE@B2k#r~JE$p4Kq5I?hv8whYANv--0Qs|Wdk{F(WQ@7+l$l89; z7`eBCycZ@qrz|Khn?l?L%X-^YC3lwFr)bny9HwkBo&~4B>!)Va_vN8@Qw^-jGOJpf z8MAW6zfjforlZf3u`4w6ObYTXL7#V-2ZCdX;MmW>vBADArtRhc{%GI!Po96i;8kQr zGj@p*Cu07KmwAfca=69b5&E{bjC)a7lzU?Uhg^)A*yum3Mpryf`-;PckavjtGh=k~ ze#S*`D3d%{)FESn?ZnJO@UWj@cPDaRv3*=vbm0`_CQF}Z>`HFBa7k$X7igd0i{zK@ z826IO0N$*BCez*0nCX6)^7o(vt)vZtH!o7=i$z&ez^$dl_{OZeDQih4^;<}!@!B(Bys{Xt zGJU-KzQUn<^&nkfpQzIX8MA@kXNHWKug^bbLMOoIiaP`2Rt_F!GFHTqpHL3IW-(?m zeubo!hm7CCkn!6~dn*{f;sN6~CIH|4oX`3WdAgz1p=0O*t}e##PT<|l7|v%5L%&&1 zdxPUA-=6ar?hYBlkFu8d*T@9_8p-%&>f^`y85Zf@YKSaBUgIA>`hE~xDLgfX--YvF z<9IIgIzKQj|2NZX*Z)W9wFR4p|7m*N4exO$_>v9Hei)i9v8%)us^wyL$}d9bb|*MI zaNQT)Y#`k}%6cD^r)q)qUg&mBfNrlnXS$ux{G6uLJqg_&?AsvS-Uy8ny1g!hZf8P| z-7jC}seo>;S0nXv49%ovtC4%xGPmb5$9Dwi_af%@=e+xOb*N6iD_B#-?){K)Xmfvn zewUEvLRljM^!sz27Dwvz`w`~;BSqO$7BKh!s17N)^QP=2q4|Y=f5DnB>$ZHiUL9Jp zF+jgtp2>2*+nDA4cgpXEesiXlPQU*|nSX+wyg-?RSf--^vb}x zFR=o|KVU7eC}Ip9*rLd|>NaYOV-e$cE_6Jo_ZEIl=XV3+Cv;rKU^jGp9pmRvHTbj0 z3ZdiW-%H06nkO-a^?@-Q_`T3^>{+JE7sBij*Z~wx}_> z&joAm%y*x;(EUZ@h3@wPb?=~kvOXWC%)`60nQO{Y{3}ZS_)+$f&-iu$<11^AtU;2# zF)+Sv)?-Zw zi7rA0R~#aPD-Myt6%UZX$(g&&v_s+-G}A7*Pg1qWR&+@0t8$Rpj4_iwty)?9M|6&n z_=z}%D6x)v)8Z_ASH8lrVVW9q#QBl){&wPczfQdGe$ImG$PgJ$g2e35sv9TESMt^-9XZ(55v37_qH=zHMcmc{1;s*@S|5=GGAnBsplXwo; zU6rKc7Xxvb>7z^z7=JHk-tgL9+QGTro)e_aTB+1}#yB0#`1lmD#us0y)*i(Fqk;B# zjVq^i6)3GDd+mx>b8f`;BR$*DTaHhUd5)>oyK>c3+3)#nmExOoe%oJ<-gR<%B@(;> z&R&`3Y~@@Wo&W190PiSAb(m8Ve`2cJj9%~8_#(-fb$)v%;#{K#I-fidT;2n?;I&Me{zO+vcB*9IrHo{^yg;dh}vC>Ht9s5PXb4Qq3oxm+^(be?jmOkzMn?h zbUPU4i1_?Q(dJU(%-NrFIx?~Ioxpcz%c}@xvPVc2ktK9-#3{_&mP4=NRDfWm$i>7F)8aio}Ij4VfamY8=yY*x5CS#`A&|m#N z$L%l!rwhXHN7jHhe#)E`)X(`E@V4)1jcK($U^SYyYrszSAVLeJojK6*gBjrt=GFgb z`&t6+JKH#i)4op`Ph>}RCuE%I>+d-KP3~3pzIx~fvg6B?gkotW7ndN5VEvQ;Fq+y)rO;RGX7o z>+6sA9K$dBjeNn;5qf;b)%#D@H*O;S{U(hx^m0yYmbpfMc(PvquESod<)PceuWz=I zGk4-QJgxhQgNx%n=&N69tZH3iscNmVC=>jdX9JLVP`5d$7j?W(iYzarS%KS{DN`+!lJfHxjx3#w^M07 zNL$K&6f>j9A>*l4T-08ZB6uQu&mq9`s)+>-k;z;&Txq>_G;*tKYWnaQu4V>tvixjonz*B*xA!zf9EM9cDa_LeEovqD=S< zc-PDkS$=#)<4?hr?EK#TeCOglcPKNff%ReLX&JnynZ8cLh8P_uaRdt-;F3jrDCHcu zQs7a(sX&)c&W9IvV^@v*dT-GR&f%i}$V)85jci>mc9J1w+4xQil%?^01nLb9&NSQ}~ z-(j9J!8;3=$W#lFshYE-UBfHBU>x8lX5I-rHp1s0q%Dzg?KykQJ(Jf(DHF{1OFsNu zuU~s}c|{WhZg6rBa=GT2n>;BXM)$W>02hT z4KnfJwPg7Fow?-8WZ;6<64Y0t@Pv z_tGZ|b<6t&yx$RaRfU~%YJ^v}PaWnpBhR^*^=)P(^FzM5T=LKs3;hfBDT@AW zr2Xr!Uw`xZ(R)&4jIe!~X`zq7enrto$@4II-X+ieu&XP4=$h6!_M|WeN-VTR`f8^9 zl9A5VDB8gOv_gj&ZKy%6QJj6bL-Nk%zsNactcz8~`f&fmOuiY|Zgl@M9gfQ#A3#$a zv^g_mOtM183UmbC@+MZW_J^ zS@<4kKQ3^H-*XqVN#;-#blrY)f#XBcjue=&ofujXzvYpXU52F0?lnql&otI^Y$tt` zRi+Xj6*@N@yM#+5X5d_*KWara_*XL~znAl-d+S&a?lo9rr(rYM%=p&yb;nKD>6t2B_FzkQyu{i2SKv+zvNOgb3;eE` z78575xsg~;;>RNAZ=Rqnv&k=bl7b9oDKdpqu(fg?*5nAsfdU2p6=m@W*1C66{O}f7 z7Y+jJ9^fSO$&u^qHR`ZVrVfMN&fU~gJwUJMK(`#KR}|f{;8M*P!x`<*Bo;(3Hqq6U zG4Ur@3k7Dqi~M~)MBnd^4qX?)OYpCTby3Q?lX^Mp^166vbD3d8iQon@pOS+Q*f$h| zD;o@vmR&px-u#BRln=TR;x=>LeoaE$JkJqa5&Y;fDRnx&=(r{RG$T&&WHLtzpuJNfl-OR%7%Q-0KU)L7tQ+mu zx$3aVIjwU&1)FR`@A7^;ULix2$0-Z;#%+mK?JOIr)LOjm!m&7(<^_DWs7wAV*_ zWxwV_?jmhIp|?4u`3>4!$oTo^Lkew{n4mN|B(!Ft_3uH)<*Ux5GF$F&z8=y|Szyze{E zM&SvLfwyw*?@3@O@V^Fl_<3eO{;nX;Uztxbcba}I^dr$vKV%-AqFF(`bxL#RmCja~ z_q~@oTU(JU*aw-11Lt7JX5lGQ^m!;WPv7tBcq!%j$5-fw^ik-N(7zMNO@#IhLGN0q z)3ub61aRN3KMyZS_x1b!@w}e>-gD1A_ujMJbL!;GTo%sf z${a0t)aE#`4(tI#qg~%H?OB%V&j!QNn?Gv@kH0gRDa|o^L6`f zOJN^i74k|v^dfsXgWr6QZ}y4*eBhf*_=3}cavKBX-ot+~cTQ7=xoy?{d8a7okC_vXO@Nz*}NATsKZ4ZXcgCz#mh>&^E!WyxbIZ$Mt?eKO| zpOya13#|8M)}1zZocxtnKj=5)EZYx0#13^aWoLxQ%$mGqK~@fdLFn@YxRCi+i8d7a z4byx4j>N~PYUv#hj8^{|Z6m zk80KIr&;<^5n~8-Z zGQR~qzjdH1KI@9uxzp5vx6Fk0ma_+SxVp|>X^iXX&uHfQ&+&&Wf&ZJPMr(3j3H)E{ zrAlXv)Wtc9^Agti68wkUnKPVjWNRg}0NuqTL}$qbCY=YJ>X)&ls7 zE0rlNTV^|b!eg+XPvbFIpM}Se@h@|)_+5R#I#X*jPO%TpI_{tMGIeB9cH1Z98@-S| zX8gSZp1`+F!}m1To}`A2cKm;n1ir>wgcdk&Qh;@^*8!f+eA2bCM*1NzH{&ZauMzpN z61%UDu_gb#EdPug=#+m7j`KbF*5!M_i{RlS;*?QtJ^iWeRj~kXD9zr}=AS3ye+B$O zJG55}9)f*tqR(YaIPv2a8f&@PwMTR_LVI=WK`jO+I{zo@P+SlHM}O|AMwXPdupOGa znfM(tx3|HEOk>PRIpydJgeF4i!N>P~(u2lZLJy3C_p!4JO^DrP7<3`+)ZqETKOwZ< z{8!K`YkJAy?pc54n*OlTwt8?jU$wY}PQ+KlOdnO!{{_4&_rv!_hfU;b}}KA!;xlItrUnxI@Q2lOn;8pBb|yq86JLI>Xg|D*Y4 z9r(}UDCL^Ds64JP2#ao=IJNbYWF|oz67|}p7cLm8klP`*#qYU2Tsb~yB0k)K_)zwW>fOo`$pA!3?jDI(F`^ETa zR6CSqA78rOUj0|&veB#Vv5TMNjUyR{%j=e#8#1e^mKQIVu?Y`5VG;inE9^f&UZ_;c zZ>!{89sUsu$n#eO9WPA7z94iFtQ;$vIPI5K0GEKWN+k6vH? zWD-9?atqF1qb##B#;WlxZ391j`4I(kfD6xP35@NT%mp@%JT;*ysXO;Aorf3sTH*-} z@*ZWsMDgpBsuHlF`HYh?6V*gp^dNO}J#5closxBTHpgP>lzMNbUUH|okE`qLPhi6tOMQOsW2ti|u=tr@+cZBE z@j;2fw-|VtOZQ6KwCB;@H)#6_%37vMS#j=*4eRYKSL8Y`Qtr1;GsL;$6_5S6D%Y{@ zcK-Pb@RE}`Q|1ftO%Lxg71*blm6QA7FEUKZNwGJ`c#<{18fB}RxhkatIyt$3dEY|L zXDhU6D=z7`(T95tIM*WMae^zJNTLO>tV;%6x*+|>k&;yG7dA>zC zSs^h~si&Yj`>kmH+sXgEeH|6j*XzI1*Isg%$57{M)HOd^IoSpu)kyjKC_AGo`z@iP z0&KpP6@?!W_1w$<s>yA6VvXI}u!h)a5pMUcJMD}2-Kp{OPI&xk`g0vI&V^5$##-szw$?sk zrJC?Qeo4>X?V9p9adUo?>zcB=$~EP=3fGhr)#xsgzNIW>vYgHC)w?R~YxY&z_X5kF zT^06i`zq|BlYdw-+ipjvdy(jM4R&`v@a(QqraXJMLRkuBE0hi2&3q?%Jc%v;p+@^N z@oC7zr{PL`8hYdQf(uWiK}pOr8r=5~Q%d-8_AU_j4ZQ(z4Hhu3V2fUc-eulQqp3jT z0!`l7R%y4;rtCZ765jjLsO3wss!5q$W^_~R3BkrT(Mj$&C$W~=SSMyWX4<}c5iznv zFCcN}E?#GEWv#H#hA4Dq=#moia$VP0*#EdrF&AX4v?aWJhi$HHrDa$#@tpFx$4)uS zxzakUDQ#v!KJ~tfen|4q`B-PHDlrjMr)@cQx6nG6AE#J|!Ru7BY{7cro2N*kJd${7#!&p(y`4#s&i9dFB9TG6kg>ZoT@i7}@UIj*?z7}h5?gI& z(v{BR=wLdu_rtwi@XlKMkehC$o)eUFoH#dvm;4C$aByP;{z~D!uh8|!62Gl|oU$kQ zt>~GId@nd|OIP+3As1dJa$<-*;%EwSG|*xA#<}*GE>H$)Yp8?$GI!@XMb9EK8}a_; z2Jwx~V4m3BoQ|0Vq4?461Grg?%n9$5sKFMft0jS$F0%dupVX(-6OS$hUn!|a>?_BB zLEgpkZ$_X^EzDo?UV|~+wMS*m4z{_K@6&-<=r#DwapDe~0GEQh*MMUMI;?c)aVm0+ z&|((Pim_c!Q*)N>j#VZeK~}fGn+zv*uHfx+`bEkYoHej!(?63!?Kem1gC6)r7lfQI zbswc(c`keP&DnZ?1m7J)CX;u^p#y=fPKk5x#4Zv(UX;LiDPp{s9aDP7N-Oj+l<_iw zb_B696y=W~@;PI=Q*bdA`zr9H%?^OpR0# zP1)4Bkox8OTsC&T& z?AZ%4ZO5JXBS0cx>8xu0i(v)R6$O8E? zXIRnI8TIiTu#4OuY8R=!ku@j)hpgev#$@;{i6Yr}$oYg%95{IPn?W^C`T?7m>=IFIZzfr@T(|W1kUY?6l@TD)ub#YmfCl zdcA4GabOm^naGjizx)B~r_94&YX1@^H4=ZkNLOc9N{;ihVJ>n}4WXPtTAUo(n`tHw zg5aX$_LR96#iHROh=W|#OiQG#YKOsAm12x)-+al;&$qyzmKoG#o0+epm?wl!k+YLI z)3Rdp@~2Bji!H)~?_4{&pn2k_Q~V2fGy39t^o8J3pW~Cg;d5}G_`ROIG~hRgQ+;mk zAFnbuGk?vIz8p#2W=CFu)IF8DWnNo+)9nwG$EbzT_>w!;C|A`+d~0WI#54|tL*4lp zdv5m;r^l(Po}DUjHB@zHwQ6aXd9zxzwTlm5nPuhQ%aNBPPsL*Pi&I|A7-S47Pj&B{ zT2t!m5;>SX=E&U^EmySs+j1KH8L*|S$XwpWdi)XNph}6dxw(GLN)7|$@fON2m>M_G zjgD4jYw*GA$+^5fZB~JsXB@-a+`FN>o$RmOp^kQcgbrqhL7B2+ zfNM&Xn&oc4w93w2`YA7hi`kTWj}l$LUSivO=#7rzgD`8VGVpcbMc}7Y`dfS-tC3aK zZ(DC)pKYPO#9`|x|0n!EPN|v0T2SzKc1*!>;5)H$P;9?*+=T)zHup==WoI1>rxD?>+mX zOoge;6Aq5Opfy=#2K701{geUYqJ z9Jg@H<%nE3pzuR;YT<{j$fZ}Bs;4?w*Cf`u#P-gSYx2hwK@aS0)?)iERfbGDqQ*B} z0Gu!Ubmap_R8vze_1)tdFw6+NBY;D*zP$TacRmED&y)s)zqF!e-)DdB33n2?V^uPoJyIz zQypM?9i5i+r|7+*9s2NW+KECIdJo;Nl{rrAbBmdO(chh9O=kbkV9ov+&6>a05Z%6+ zXFCm1RViw8ySy_p?vbOlZ=>m-LA3dv_sKo6O7T37PWJ?Ic5_78&Lh#Toi7Y3+xgN3 z`*$d#M=W)WzHn*I=#fhojUGiVplQ5cy(7+`o~+fzRdjm|{Bkwps+l(3cXH(epQwYY zb{V3p;#Fd^tFcXr!PV`!%hjcfc8pc-(&I14+ElH^wu{UxKEaRi&ug@02meL$AA3`q z9%C<{>}&f3eZeP}-fO>?bOLs!`*sMb^xRhi+%w zagFRL>y0hJ3_s0zSY=vX!S?-^RxKt+%sMrb93boLr7bCwOUVtBr)Ic)msZ+~Po_*3 z{&c(e7^^n+LJe+ONRJnZPY{;gGfo3dITi*a37 zzZEy&ZkvnwXBxW8i}#n?QwJ!AQd0GP7k`7RD6g4um;!yv_+x&4^C)9f#-EHm^`ko; zNMqc68=98$454ez)4TL@Zby*L!^h#a!Xu2q8I-LuCevM!!&Woqd&i*K=NP<_d`*3g zJ)xNtMQtyK7RwoXX^QK~6ReTw&=XB3Rz9$r`y%SPpL)`PZxQgy`Nptuc$9ItfbY^3 zTh$_EaML2iEb%kQMQCb@U@UH#eD+wpagEY54jrj_{ErlJqq*p-iw%Y*7v(6FqmWZT zWzAE`UlwPGv=L|Eq^yOfc##W^m;@hQv9yi2R3H?`qhrNL@DQmI#Veeo}zQLG$ zjBjH3M`9rCV_fcMTpnXwe$dCb+{d`wA2KfYvW6@0OnVuZF4m>T7?*EA!}l|;uwPZl zykS$L;18mz%8><%$eSSJvmAb3<|!GU?acFg)tH_!D*r9vKN+L=;K*M6CL3cEz4Vmr zs!dz3szUhX=iv36=yD`Kqwr;yk>52Pe770~S9ysoGEKF%FGLP}nd?^8?!5*}`;mYg z<>fw$IiVf?1%KEk;SFa=JO}F1);ajZ_3(*{sZaE~4&WUN?u17Xep7S?zw6N%oXxL@ zZ9?No@Ke@fiRk;nlKI~}MRW!MKCtJ#uJ@3>35P$8{tJbFGdLGsB@@`&4A!dcv_s%8 z0rpwIzE(vqhnzGs0CzTU3xDf|Cd8L#*mZi$ZG3pn;#IR*@6W}Pid+?x1PKxK#GPN(`+G@U6U0?#vWDo>q&I7~ls~&ucq~eK*Qg zUpH!MeJ1l>enUo+vwOjV8LKLrEZ9KiGbYW%WXlNbe_FtJe2Bf4LEG7D7l<9UkoL`{ zowJa!kgf3V5jx&mc6ogVF=T6U$pLIs$p@R@eS-S1(~Ot>1q;}xSz~;7YJK|8oZGX0 z?i@WqNnl(REauD{RZv3wavRshDmrNVhh}n(9v`{Xl39%YQTG=Yd(vR<6-g4GwQo3@ zY3KA^*!Ha0R2{Bm>ljnTG5U1@?EyEtl-UQ^>k4l8_6WEUeZ9yzDNp)0e#*0Eo^7EG zx(p;XJv}#TGWDJr=jxn9e9_&iIgmSA{Mf~&C~{$<_t_KvjpE<2+|Y}+GVm75Gr^1G zY!KZ1c(Afzvvz;>|Bv#2=yxml4u2kx(x7-I+Q}8j^>{f0=U-`Oe|YG+`8oPXrLTz7 zx57wlMPl`_M_Xd``LLVo`z?F-tash~LY?&EDeNv1OIv&huK$1>+4NuKe%F*dtq;UU zMfAmkynX%u$HBz(O;-;cdmQ|;o|_v?`rb&{tBZZaUuLZLr7Ipw5q=2e=(E!!>iw4* zI;H$yEmS;W-)Jyw*d%3KLfcM#&rn~!x`92%6;o?@rtck2@IDVtnki@X-|*Gp`NMqM z7d+CQe+3?rLhzu|VlN(y&_-W%ccH@(*lPa;xHwK3{oz9Bwj~h1_*(&dTz3w9_z##4 zwWdcAUp*TANDN~*7CMQ8PU4}Hfy`MZkL=+WyPMXB_@tV#3D+5icI+pXrqD-TU`}g> zN0GV4&`N%Tz zeUfJ~2i8O`Ut;RIEH<5^Xz|vV)i-X5Y5p+s=G4T?VsqBi#W=u4HrEE`aEWDbgt@!U zs;?d8_#~tf>(eozhS-zDOz4W7lp!$_zKEQpnxbNu_hZaF&-=nZ$zNuu4~6?!tg`c} z?Zitox^}ug_m6KA|F9Lh55JXt1MT>swU+6ATkBZUcTnf74*xSd3}R2VYQAsY0rI`g zy&nGpFZ1erl|4>e*U7w!-%1mDmQ?C@0F!~Uk#i82$g3Nk7F?2dB8hiHc-I`ckGT9J zlkIZNd>G7qBKd~I-Xnd0Eog(Z!8cE2GuQlAg8Gb?lkV#F$VIp9iA%AZZ*y0ExW9~y=*|e_Pim@ z(^eLtU$08%+Eg!if0=QT!k&TI@ULy)trZ%-ZsmXkbC(i7lylm52G7k>EbelRzqJ;) zuZWv_YxK+Z{6We<8!@HH|5^~mx#|+zu$&=Pw$l}=M|3LF2u)`__$#~f;0Also8|V( zH&@wnD2wmitKdnk)UB|`Q~ZFlDC=~t>dE83Hs%a)OPj@CzHe9>@CT6pEQ+;2;P_32 zdMFG0MX~=KKa7H?L}HO4kB`6?y2H5M{>vEU>3V2ReC0~nyU7N8_Mutb(Ol0`lM2>b z5{E_eubHysKU+ezy>3vYU2-?AImkY|x2o)C-dtzTIk?_F&1i9V9H_KUVei{CW0Jd> z`9yqwGK}oEqnv7kvTSBerQKS)+MeNX;r|ntP#>W2_3-lfKXAq8cTbBi;JJ*6sk9kf z1n~NbU2q}zTn|4JE$4|*;B~!?m^hwE^oONeiJ!uGH0>3bk-rKA?rPwkj&DVWvC=+> z*u^rg#P7BZ+mO^F?Y)QV(Dph*+FL(pZJ@nt?Uu9JYo@(xX>X<0-UY~MU)x^co9N#t zk&Q#!3*O7<{{nolrZE?^9{A-ZnFn6sUh=>e%X(ilLSOI8)OuO#{n)K%!Ylc5iEDvu zd^z~)?>ir3sS&^6MzP6b8`fz^_^L$B{(DrTtx1_FMg>2xYP-)^eS?$zjlv^N1Akep zG5AWvr_q;M8OV!LhVW!!CzSsu!^038`kK%>jVq-y!`l&TW3b(=w5K&mkE^1n7f96WeZQ^aOFk?$Hu-*&Lr+Bl;HCH*(<+MTHnH<}*raH?9 zplA3%9Y=mj)~JhOYb%nwMzj8Z#J7U0%@xX_VSD@=g~ylspaDCSNTKmCoqQp zS2?o(|GHP?(U(_Z2QqlpV3!!p+9P9h4C~KM=qHUB&oTz<>a%ZeVy;A2k-6T~F)7z& zSb8HdpJ#BaN!`{HgJ=i;ttW=h4$7Iuc^2o{JiC+FL~F>ydM~kwUPp#{kJv=zYQgO_ zhw^WaVy-TlS{swew-{n3X7YV0=kRvOIxf$o{bs(A>(^+rOYj&HzguzzTn`@iLDRlb zIbXqt#Pkl9lR+7wc+3Tlp*ZQ6PtF~9NN6J%lTBh+>vDkjIO#TcJ%#P+y0GaKK1vrT+S@;I9~*TC;z`A*ly1^Bmq)BfzMP8g5G)9J)*;RwPu_ZIk` z+?+t1@I-G8YxO?ry_I_9yL)ybH{?p(nX};+eE1I-kmn;A|7K{%!dS7w8~2WtBIsZ6 zk_zoe9H$!Nucy{9u4UcZCG?FRZHsOBm|Ej)V{SyIpTRK_+5H9N`jN=>YhHLHW)yP$ zniodJtU-p4<6XpnsHNL@zKv(wc(x6>ehqT{R^nY9OVatlI&x&nm_NdrEA5=RoAH(o zUy9Fa_lkUAoe9i&%vsR;0Xc94`vrD12j+*euXj=orom&~I@V-Sq`u4ZcGHpTHow zy9Blfxn6#u2HOC+E*_a&Og)mvD~IPf+|T6R!M%ff2loP(!o9-1!u>aas|-J~zF-qN z7zAvCfhh?*MaUcn?wg<0abEeEpE>Tp)bRelNXDh`{Dx782Pm>lc)Kp98=V%DL_J#x}!b_q_IY)L$hWQw$WtbA7qXdS zye@my4aWzNdAXQ#sO;q%)-h?Xe{y}zQT5Q~T#^5f5p3mGoOsl~Q5i;lB%Vo(u;$0e zHN^f=$+@#d`%ZLPL%jD6>)t3jmt&GU58Inc`}fC?3*&HF%naIJbhtLgKu$W>v6`{6 zE~N8J;?aq&>{pyO%36W{SIzK@v1;~$+6~M-DF@t6Y#8BREus zJ7TucuJ>6_BnF=aKfndUM~szkt(0Tq8DruW+Yg2ebjDZPi{mTpZyPKHvR+C4jWY&k zfs=QVb1LWBULc?4!H}{?qpzC&7~?cq3TPYmxC% zf#(Q02f=ep<1fq!tsDydTVqV>sv9-Vc@(*^X#D0FSue9FhdnjcS6Fk4hHojnmiw3G zAN(_~=l$d|bHXH^eetC+{toWf`i=3Iv5rcv49Sl{o)5R_K+f%>u|2Nf+v&z3ZXbS; zriSd>KjK+k#dW*onW^D>x4#M>P;0zyxBT-e_JRqtDYTps4LP@q&AqR@A>Zd)6Y^)V z@Unx157liEQeJqyOOWja2FXDZ`h76&T_{ZVAUhe=VP}1(`$_7&t9%=TS@v<(9k_0{ zNyV2_bf6)y$i6_~i7VlSQ_?HlExgYl4v0}TU0g$3b>4f3cNFbNPf>1xUVEO2{44$U z5Ih7i3gS0`$L}Hcma$*F2_EOYWaXAr>elMR&+mm~@!3OPMjNF~NAdZTy8V3T5}uv@ z3w|s1q7mvOtG;ib_uI+<48{wTIXY~1#*7Zf&wxX8!^aavH=5}ESk8>CcFx@^<}%;R zVcxlh`R8ipp{wBG3(*zLUg9_EIZw|O({jF|xIf0$B{>Gm6xDqX*LpmE;fWOQq&v(B z$Sl|R(<~55MESlJd*wqg7h@6VO*A+C)a*3;q^>KXPgnl+d?Y>=VtOS9Fu&SF7jE zP6gNKtKxr6Srx`%1(NGQWTm0-jiEFV{O4GJHhjSL1%6J|f&TouIu7t%IpykYlU%lX z+xm;?_g}30{k@a*BRn=RYYe=o9$Pk#wF^DYc-(HADA9~_X`dVf|PZqkC1!KVrJfTP0)uu#$?f`U9iO|&`=xZ=^ zmc+U?1bJ$JM~^wE>(PZyv~{LOwj0)wm+Fci3=cfy3j7<&Ov6%Cb=gA3jyXk{V_uo0 z-D8(vKKh=>b7#i|(8j0i+xsqcUPs;60>gKp>v_mG12h@WymG+Y8uldmvXm(q%(K3u z19odZEL>6$h9_pBW9-&2?m19-ArFys~&^Kok|djRk)wBwgS-p9@) zldJRBQRQ2vmEOnXpWHsPsX+fT)+x0U^6!g8O?ebV+S+RnGVI!C?z zLyUuLQy53n&@Dt^BQ!Tx&uNX7{hela7xrZ0v33?Q{$!7l)IErLWgplX<7Jr-1?oRT zy=ByU+fxtOJGFYJEU)`Yy<+R>#rp=sP}>S(36=%w6@R?&{?69M)NuOyd*JN`@OM3U z{4V&sjy}H@xjs_s^M}Co9ao*bw@}yPqpt^#7U<0ky;+$<^H-XN<%*t=vxzhORwp*` zYcv|UQ1``OU`VtD_l64X_q9i_cP>=DKbO9u93L?Y43u>YAHs^^%A7{Q0cGm*wjN*a zj3L1$d9U{x615l`&1tS{LS?XE?!7$P(^tLW^GD}B`?W5t5MyEcbT z7!dG%)Z;$M{25x#QFOKPeFOa;`j5Us~w&;DN=TR&UnHRo`TrBlm$2Ksn zUsy|Zd!+cA%G#%|`6lL!5#%!KuTJ^@80%^f=3rR@uaqO>x4(aVg?-bL=&LHzbea;{ zaR}`|Tkw%{oX}Ky2yJQgv5!FN`KB}_a)Hp5(9}L~p@z^^uw1c4O4;X2Q~nBl-v0bY zgBVAH8Ba-!E9UJLL$MtU@bu=R(Czfh-Md14sgqdq^RQ#dny)fPS3=(r^i5#w?h3V6 zHK1>8G+=XBIcS&({OIeBZvPcp>0WU?b$=Hat^<~98PkDzykda9=K;Q<209kL0X|>|Jjtx0Ss!b-BQ=&5btZ zM!pG!QOE0W=EmVYb7NzGZ<98C6&6|dbz8c?(ict~wC7yymbvDpLh+}Nei{T$27{X< za5My5(MKy-JHSt{pCopW@CD)hG&s;t@6k`SU(-)T=+=GGPmW6VWeu`f$z^JG4jP6p zvkgD@0@>T8<3;?;_4akIxB+;s2d?h|-*v!wE%Whr(7EV+H&n`?|Mt<|dV^v!-_%=P z58nDHSMbK#65pZmf`c_)Q26Rj*1m`7$F6ti#{y-_eAb%lpL*Edd0e@z`kg7g{a8Rh z@=Ykb(vNSm*4)5aGe5A_bh5tr=u3gm%(#_ypBu*W?c4tGB7OVGY_Z>+dko?y-6{LO z1H4o3_^TM&q4jf6*7sZLKVM|<IV0Xu|$i?x2f-&miQPHb{?@AJoL zGXE}fK<3Z$qYo%Eb41o>;Jc;Spof)TA$kM+{hlrl*tDC`y@?&WYG|(=TlVJ$?ZEqn z5PMAQz!j=zGxGn0bluK;m@-5k(1NZ`o{!`C0`x+z$h;*H*nvlKbg*aRR& z)tp{_#ru~l-7h1v)^P|AVx^B-e?p8HWLSw&j}M;vgCupQjpr8N@L_)me^&~>(n_4* zR`vjzRW%-aXS{DztaCnXtw~2eaX@@u44%d<{*50bW$&yZ7i1;2-^wWTi{P{cxMYnm zViW%n_7M3-a)Rw!vu&)pyCx+Biq!;M;{#Q!I2 z0m4iC&p1yr4t6iVR@_7zM29ZlmJsh(bPo9|fBc|rw5c#Zx@nWd9md|=POh#^?ROVF zDc9FERBf_iQ$v5O+02AbhEGjMa9V5&Ss%=@&%q*mhQwGKs;$LB_dhWt*@#CNFLgJ; z+dYVGD->7ff>j~@MiXrkJ%10ZTRdU8>pfDR>{9w*W@@GGD1UehRGs%hI;wYb5Q>c&^!AxER3waTo zriZ2>Nw0*D)~}dCPct?!OFZ&8{Bxw7p{oeabrqU#$60IT`K$}?%`$E1y7_m^ zw<`0lf%z93+6ogg7y7)4#6#knBKb$oK&Kt)@cA4g7i+#qyL$A4ezo4me32mfLFNYd z?Oi?k!AA|u1rha$q945c?76~+Enf6;Ejg|(L)_TpqXVU|C8Csx}jEYYBl_7Q)6lI?tJ>)s_eQYg|T90 zT;Vr+P1X}G_TyXKrQEa6?V9$Vr(9C4;-5iGEA|4#JEKK*u@RHK%I@SoZ{>ogMsw6% zdO7hp#6E)EE>}JD@@Dck5R+Nzm%3Z1Q~da9c!&JyX^{8Ct(W&LM%D{>QlE{o6|*}N zd901SP9i7G$6m}tF|UdF!%ZIECe2o?x})S7_G^KaJu>rN3HXi_aNQWNs}pcwUJP^C zk^5ZEW0WPbS;JxEar}U#ULP?_q#uL(ks93oji)PeI#1n=98W*$wjTCLFi%}EnA`yL zz1R?2Y%b?ubZVlXt5gPOg17N?@H+3s==+#AGM|48eEOV0Jdl7a+>-8cBEQ7Tx4Q05 zk6(K%z$>?s2SWHze3;EIU}Qtk>U)V~oIGOqDZjyNt8K zxl!;TICup8L`5R@s@{Cr9rGne!i$>k9(vdN7u6(?Pn;MImp)4#ZIztm26XU7_!AT3 zCW3J@fN|4n`}G@#9XeWNsF!ggvDmcr(@ERK=c{6H$J}?n+~{(luRdz@%mw%UvKYol zG;}o7Tgp55f6Z@I>^A-8DZ%y?nx{lD;p>XMEBMTc?ItvriP&ytLHAJ_-A6(9>ovNc zrbfA)Ttm~>d<4zU=RTkHgE7FK5@JSk?dCcIS}*1BZ8z2n4gDc7wrZho{~ryV{o`0FBp74$wHUwKN*f)oHv9`XH8^#PZT;yfi@LSs^t3D)bwqaq(LV(zq>z z#@kqnqcj@7(s_)sL`P<(PiquqBKdLNOyU^CF`RESnxCkQmpzI#y*_oj#HUWhr_PLh z-!Nl8{&i`EzFeb6W$&DdeOhrTOBK$k0sDa12E@N9k2<8!8eh@JxUa&9eyVcQY5vV% z%^a@n4M#MD>APtCng{JZUt`+c&-$9t|o;z zWtR5xv7(DqI(^_(d-o{wEcP5Hb7CfT&dXW5pQ2A~%$-rklyNfVI}aF$S;u`B(<|Gp~!Hsy4;9LVWD^81|b^uwt!i{8x`yXf%dv5PA4(|)0zBQs_ZcHCmt z%o6$|Ga}Dsi)SMCeAZ`SfUr|R#a+h(n-)Z|uT8&1O} zbR^-Io7xZLbhgmu)~1}!O!_R}(P(dHOkoonzd-%bf2DDxAFhd6bT~c6ao7=)jqNH8 z{A8q^&ipWC2M88hrnP$#Uq!Uz1m zcMP5CeCt#4lw}pXQ{kglywJ5#=2m{t<{dp>h}~OG+0B$E zxnQdBMYB`JXN)`9TPO9k14GGe86DZoX#!7hTr@Yi_S_(`tQi-`b8att)5>GAyBs#x zlK(C`j6hEuN{8z3&13gLTdAq3$roa$5<1)m?I_UUHgv(M0XkHmy$iYTiw;vm=&(hj zLq`uCwrFKJ&Z5IC`oIzC>UP3khSFiiY5)H*Iu!XLku|$NTKwPn`kbTlEn=g=-}S+q z`qAY2kvXUHjhxOGuwNwRDxOa0^K;rJ&-e1IFWMG6b3J8h|1vH+k#nEs+jG%Rux$CK zo^OSoeHuNRHF|FBGH&o4HF^|y)psee*cMohOZI16~5;F9z7m>%>{o!M*p_X*IWP`y?o6O?|9z* zyZD-qZ0aQ`@G2Tl!+z}^o`&lk_)2}`fBcpnzDCS`PP3wU-Ran zUcN^5djuQ~Y7@-;sA8Xt8EZ)1j@b>3!(_t8Ipxl!&Ddw826 z-pzq$Q6ap|`aXG^sCL)*Fy7`VrT$OM&!PNHkgqY*)=wFSpC%}sA2D`6M{elAHdReL zhXofa^B#tODN9n86;F^d6TKFt`~mjzo!EJCipQ%caobB)OwPC1s?K1CbqOy6pY;y1 zi`du8S)czGcGJXc_Rvs|oUcoe>+%Pre0afme+Djzqa^+>@?6T1cP{oU{-@D3MU7@} zBlVeCBhAzm&6lW2ZlE=4~sMn=O_q1z4~bqF2=QE4kE@B>CHT z7S6YQ3jZ_(IHm5^K-(Rx7k_4M3@u|3Wekuq{snxL6aG@yk?XuvJ!PApSgY|<(g(e;+iYP9Ql7we zlgmkXs!!pmMBX^pnkYP#{G;<#2^wG322Zx&pX95~MVC?MpvM>Cq59*m-iLn*<*#lE z7O@L z%7ZrxzLR-u|E5ls3` zd%m70%kO6%`zAcu6e;8X@nm1C@A{v{lQps4{UbbC^m8o*ZF*TvZO#6|5JIgFVFPyWbeJE^JE?9U4$o_DSBw^Xin^C z==+* zQ)+gkCT}GcO{U59d>+@+xeoe?DsiKeRZD7e4u>($HO4*Jkz77GEjeOH_84czg~_ur zMkco#4P#m_HIAvBoSwXY@(A?i8Og3Sne5MEA22#gJ3Q`eXDZ(*V_GMvW3b1fGY~ti z_`N207rf)&xNb^ja$BVG{712h>~ZaRcRlYEXI66iBxOu~*+8|d}d7XR`}&A&WY zhvXB*&%8qR3Vzl0`s!(a((YXK(3y063i7cpe%sI~HgsLbo9LbJFzsR=3woD>>c&+z@1i+C5wekbt}z;2X^pN8Xy5jQI@)D}8^sB(4`68EQ2vkS7PumPK38h#u( z_;ILH`G#}$h^VC*_;ff!d^(&VJ{`_5pN>`NY(s5^>`v0+M>NWQRO~RcePScF{5(~8 z#(@4#WM|no*|Yy;YGRCO>hPF9zS^}>Azz-fBN!{^&$Q>$9QIs(D+imNjz7U&#c61m z??k7ZC}V}|F^rY|a4NduK4Z63^xa>@{q|Sb-p)__Nqftt*6VvUWe;nj_q9#z$pW5- zfxX6dht?N;`EqDB9&FNTv?)ftu{Op>v3~{0-zkm9zBM&3*LGGU=Zy>7U%Q`{(vgdi!Sw{|P=`{I6jDgyE3B@f#)H zm6`EoVcfAdc7=^OM)t=>u$SDV?HSG!J!6kA!SIgF=sOJT?MuaeX=G1Y`tXHgO-lBX zJF#(A4Bs|3LbG*l8+#M;RE^j=@t>W69rQxJd*s;Gv0I0KF!oMto+iG%$t?EJ3zH*= zQ*aZur5`e%?c@GV;t|}%{g2o;zb|!Eas>BR^6dhyedtGXDZ8AqtK^z*q@E19*8WdV zmK<&=*f~#O=fs~PQQ%dgT-~O_n`6>B7QH)S?4mzi^owHjc;d%mG`qTQ;`tivl52Qg z$#W??2m7Z(4(yf=o@em9i8*os&s^`Oj#YSO=Gi*vpe2|6yy$UHAMV~Lwm>)kpD`+# zr+LmA8rW+s{@MxblTKZ9`$Lf5_eisTL}e)TTwxoV|riG^HE z>0G9~w!p@VJUwr!N6`8Nr_z&}p8uky^wvuz>I zKH^yf&t42Xdmnm@!Y1r?ylNi--9(&HVuf}pux&4bHb!7C&pEu6BQ<6^&xM9AQ?mhv@cmd{a708S9G~_v^5k=YZFB+^-MZ=W;LpnBr$C?`Lp-jCe!jnaLc0U0rY! zb>i_kcY|~GK6*xi=Y!B{+}r7#FXDV*;G7y0pFAq&;(H$*yJq;ZnEP+b>cYpyBR-iD zo3DJw*0Hi*6xtronvcCYQ@2-hEj}D|75^dhc`%e05<3O7s*kSBZg598!r>DEMurdmKhx~ourtT}aN$M9j70hecTRfTIKaM3lUj&Y}h2ZJO;3kNxEnK@*^$>9wH9X;$tKq3u)=ltK zdsmA1h4kPloL_^kwEa@&Ltm-pKIkiA`ZuO8nG=HabzK05#b2zNlkw+AHD2O>I-M2w zL1+8UMQ4_8PG_Mwis=_e|M$|{x%L_RbE6KOA>Tj;ywhoTr!QDf#h3Z>fKMiT)WIG; z%4qLMCFV$T7V<;ZzAI{pr>sn`T`Kn)za=@Q%WU=7J3I?=;2-b@5g(+(&vk7q+N^kf zve^tzk>E8j*BUu~@>JJG?Z4VldK`rBVYB$cK>R6XnXkeCUsAcLnX<%A@{7m2H>MJ2 zqziuvVik0LIv?H@eq{;#Mx~8CVED6Hh-;M^VJI6SjBP4O@% z9I}z4jaUMm)}5Rqluk2qG0(Mp9pW=k{QA@?Gr7k~@WE}1R4-|Jket3%97k18D{&f_ zYdihKj%2zY*y?7F17q4kdBy2}vcH`6CwtqY^3y7Y3_l%J^PiR7;6%rlaw(D5hxOdEL)Ir~PLovab}n;S-Z#J@mt zT8MvS;X!N?U^{nB+}k zt@2@?6Id$Ajgdl(wrJ*&Av~9uaMgxkr3+|}FG=Dd5K{o!YG)19+F=o&R4GUHA?f=T zL(4`E>M2|H!1)b>-TA}}4%V{@d+J)+ni22Xvo1lWjgPUL%HA1ggy?CMr)|6wUv-J& zFFe~iV3hX~OKuf5RcP4zYx+>j#fM&p^|rm4wOalgt^FIN<*Ln~yy4Je7T**@t3t1N zTq9>_XAAc;x%R>9NV}!|os=yxG$hZqz%2KIpT)#k%m7}q!xb;_=mbYY)Q9ZO>V4$J zw%Fue`1kX{*HmE7U_Y~zD|D0b;Jzs-_{h59i-n#G>KNbTtdjath#TPrX980O@ui?G zTSkJSJ#PbVnc!0LBc7H285~sO+ba0W(C{;~oqa{bme6zwopQF)b|-U^yhlf~O!k0Eh% zPcv7xF%Er^dhS!7QQ2bwr?SuCkAiRZU$o&P^9A?v&0gaE<+I1wN1kiR2`+VuA4@B9 zlK2f@_5`sFjf31-_|xQZK1Ls*OY3~6Fvr=oFvn>zCS+RB6M@rs@dH|6OtAghI7HiT z%|57WWRH6^{au`{)6-hcl5)IhR6zXXu+?zql#0g7zOac9Y9eITXyd zoL}QAnW>cTY0W}=X)5328>bR`jrj4ZwJB32XFBtB2646X0&%sGO_8@1&tC`Vd5tMc z=Sf4#MAx(jo_>#Sl;X@Eon6vKne2U|Ov^dTYz!|GI`5Chg+3+bkj7uIXELQrsW)dU z9`Tn^XCS(-M>NjM3s+GM#55(4R)I zAMGa2Yo?Mr2L0$dbfaqny3tJbyR8lAMl;o9u47^{+3(g4k0kY!shOoJ>unBu?sD1l zCiRz6FZ{|xksUKR2kXzL{(S0Rrzp>q(QfhEwy3T>nS3W>Vh8&UZzEFz#GA*))+TUtu@~8TusFd&!wX8 z+6Hbn=icz#mIF6D=Mo(s_b=QuAT||!P7`&?wTo-Fc5R4NxHbo_7jRuiof11r_T0(d zI++jfS=gma(A!y4A$oVk^9J-*&N!F2k+sI#m&kg)mzcT=x(|7#a&{~)icvVs%Aeje zcb)mW?5}hjURda;Y$^2psPswSzESu_;koE$ozQ^nbG4w05go4V$!krItXH6Sk>OP-V|-wbCGRsViWX;# zwh?FS>^+uwX62d6%Un}T5$asl7tOwMlr=}^I}^M{Vziwa=j!|^u>$40Vq(!3KjJEJ z8_M@=PruCB_Ve;R^;wq_lhM{R4H^(!%iJRKl#OfVF0UDy0N2md9#9Sm-zB!2C*Wa^ z5-<8#8GO;D!v82IKS-*!>wC?G*B|7SzIn0BzcE;E33d9)sE>Nxnb{@VsaNK(Z&L4r z)VpuL;`x~YTQcpvm9y|AQb%PEf0^Lj#&<%qNzi_1T>M~;0zb6z*;VWpUBNn^tD>t` zyv>Ug&*X>D`B1LV`I}q^$4!m#zoxdP%iMeZ<3{*ZZ9H(+#slXBa3X8*rJSWcp-+jM zn$EpJY!BSG2@JgRX|RY6R_D2aSz8;rdZ%LYg&fJk~F0Y>Bdt>)45}z?| zI%6^9zIbc!zD|2@2)j?)Fd}&^dLYSFFyxglwVVZlW2x83@wYz&pXqj(w>|_1d|So0 zbrm;=-tg=_!o-~R8;6`z9?4}+Sjd>hKX++r$k>+gm3_GAD`Q*6^V#EDAKNmXL&tYt zW7`lI+b(U*VqZApTYU6qGJZ2T`^pTRZ54*juZ(ZLGj|fho$neuB!>HtLWOk=Rs_SXZ+*k{*&)BCkOY*%6o0BCVJ2C{M65L zIzL;%o{)6r2#%xttJ`4*d5O=CUb=ivN4J0DXN=`U8}UphxjKK2jQ*QkiZSCIp+bzaJGOyl1dB3TM?|6H&;#r;}bCB36WUZIDZTk1?`F;W4UVFEqeW!n; z$B_trQ^7az0NOXdq3pTF!A%Rl%zcYFZkZd~bf`|DmiPSUnT$-0ocU(Sy4<&%=o4Ov zF;~qRI;w=%;+A)?k+iod}RxsH2;(wl;k?8r;Lo-iaY8nuIcbUYUpg; zLLZ5a;d>E#+U9alzQ}972N^5(8IT>&FMdSk1p9c76v6UR*S@Rb5$*~r`&wQAAK=FKAW&Q$h(>ePNsG2SG(2L06T(@oX zZ_H(#A9;moLtk-%WE{)-K7Wm}%*q^HO-`|O=o|WqXC%Cs$Q=WL1Ae5lfJ5X>=62-I zgr=nKT-pCff61J!_wP~GjN;cPRVAQ{kr?5LYNDkbHRyA`~N(Z!hashN|VPC4)%WTCZ9X5DxYq7IK*5q>P zvG9Br<5lc7gDBG)6;(AGJ(}QB+IAJ^e9n8}J71%HZR$F^T(>=1Z6~h2H!8|nwH}?C zjXD=o=PdGP3JlUO!c)n45$$dxZ`mo@G~c3}Jc^t@nlcNzv)_v5zn%PlcHcz>`fk0} zccsYsG1T)K<pb5TX!m>c=_l&F_Gp9NrytYad(>#c3a|&BwnRlm zYwaG(_xEw!&%b4A9B^0JkE{3Va6^|?V3-BmJHSgDeJ}Moq5bW^cNqA)SL`LugyQ** zA~IV;onxvgF+4`0*rtN9vegXNo-GDl53wEmmd7AVi%tP~TJ&GVhC#NIyi(sHe@$k zam1$XG@p3%O|xO&-+k$_7gh0$yNorMv*^{{<@!2w2mAK#(_$Jo6Cbt_8QOvj&-dCK zp9<_UhqdG?p7;5W_><7R*rxS(j{~63H}1yw3%R_;pga@4FHmBzOZ$4tr5u@yv~t-m zbu3V(zE0^j@Ax9h<6TX+|7mlowy$ZRS#90T*jT1Qv!QtHuRijKgU12jITCym_kIO2 zy@BOZaF#*8R1*6)$ScX*DZH2+zv;R(=Jj}GSrIxDCANG{{pF?&*yhP`G06K8d-yYG zug|LNDVF_(j*%XpLFwFwk52Rc@~NzA6ShnxpVLCxn5^t+Ud`MeK|UCHA4BY2`KMu& z_D{1xU%NA?b1bx5Mmga*xg{66;7(#zK08=qj;V)MQGYFE3J+P!{bgLAv5|LxYqMM@ zl<$#uk8th#Me4&A`1)z^?FM3uoS}_De)Ouh@DWGf(n8rsVso&g_won>-|pI|w^{Lg zYWi~HDgJx*Oynf-kC3uI;hDrR_&G33c}29LKiK>C8`1m03?Cx>An_<9mPhkZ#k2eM zu8j@ARY#0}?^u^}2>R^|)m9+6m6PDJgnk8vGwdU64D=Uq<;fvLzAfGfj=n26O4o68 zQocoxFF4xEwMPHEvkH#TPs@H(`F1z&IxcnXk@wf~&V0w@v+;Kn{BgAK-p@W+^tf*- z1C;S+G#pN^-#Xak4Dv+{f52BtVvq#dFeuQ5LBKC`l>l9d-bKp0C_q=>Vh?=%385>q zk@fX}(lIDk?GU<(hOQK1VJg%ueAi>ZDs)9zx*Z6*l7AGV@QjM*0dOdKN1d+v_6-sE zmrEZ~=52x({`-Xg`tk=6IusphxZbp9Ji#ML|CvW*msm%;O0fSniM-jDKCGGfQHQHn zpBmJC1ZhF~V!y>Ujl{tLt|x@GnQ9hBK;zATsHH~ z{<_VFyKoaIpCfn+Iln3QpM}&dGO41dp8J6_*!RL)m`#R`MCp@|GW+{?RUiLGg#Ekh zGE<^5-qk5(m=~xX)+^d%_U~w@v1P z*TlDv_8-yOuXt{RmiG~tT;9oA5jw9+uBTi7*tJn?V!lWET(|Dm*Da-Y-NIHUWv!Yl zxDXr<_KuyxHFX~AqmJ)WeyDF~$NcH|Lh1A9UZDy8nV```um5Q1HjHF)X&?ENhtqfQm)CZf zRiQUMF8bHeH1OVUE}Ep-r_H8_jyb@!V6)=6nseyCpLBO6h1V7SZ*Jh8=O`4sjkkXa@G-z{w0Q_9(&bgoc8uJh9OnNW~}-N{x&KyK)Aok z=RbN;;|qiQr#2tKLy9d`<~;20C!5j12~R3xIsChv;CIL%=lX7S@VmHw>^uJf1>Kqo zUq!yj3it%*UFb3hZ_6~r6NGtzk(^M|h(|;|V0eysDV@e;;*+e+t+cJ>QW`X}punz2VOUZ>hwckVCE|KT~Ml z%z%xgCw_$Z0DLxyx;eIzi;~ncO~tY<2<-AaK-a=cpu;*lUIt?;C?f`QGC8NctZZn3zfW7^+FciD zx6roGqoz~k`wxf8{*SZcEeG3sUTqykn>hA_w^g2h!?qrp41Ozm?R7N=Ql1OaWo?3T zC`dmt4+L#-DK6K^I%uVYBje&qdrF7#q;g@kU89eXvZj4zJXuMf);^UoS?D_o`ph8r zdn3B@M&d5l@x1mx%JPN06B~rD_u1~vJnMlYNjW6=3x%OCJ&->)?oe=SX>*$sx??V_ zs~FxfxSaKX^(Kuxoqj{y$y1m7YEutg(kFtqpdGg|NUz%SUi<73!=R?-?#M}ggFX)g z{Dse=VbNg#2YT3Ezwltp?w<*+bCp9G8LoP0|AezB%VSAZ>W^WgwwjgBX?G~~KIR3( zJ!;2%#n?XcqKZxUvU+Dm8@M;M7hJSv(?`H$zIgp6{C3*S7q8nS?K5MC6n{|-|NrLl zGe)@T1}A&*H=G=`mu_o?71+$@-T~ypA~GSM=PoFFhIH4}q`oTBZJv2DLs#NoY4;^x&q(G;)B#7xmRY z=L^g6N#_sCmVY5E!F@O~kF-F?vJO1@H=~9Z!O_dWC3DkRIJ!CIpn$})2ur0EjMyY+3Qe!*t7zId?SFXxNz zzS_7nq>cB7!KKkbKW&`;HEmoOR@S-Nc=12j#&CW(=vN>({y|v%eYNE?`~jq&t>8e@ zy>Ok$wdfZlKBDx2*5A;2@f@Z8r#Gne?-`W##!JdK6>GFE>&)4Dv~$5U`5VCW{$G1x ziuf19BsAWjwO#c(SPK?Vm+(v?uL>_M_>1m^^x-GDigFgdqNY8v^TJxDLbL zLGwR>zi?P5^a1OUi9KWUYhk@F1lGs@Nm%>f`L(tRttCpXYSx|4m`f_t^q5HK4m*6T z&%rfNXiw80aK42$+=`D%*)I(BW%z15P5*@cSB!2z=WBGCLDqq?FV*_8o;jfRd0*w< zK>6YShr_SI+@;iq*WCy#!McJni`3s&Jud3$>)Y_U7+3!=u15WvaTR+Wxatf3ThFtv z&rCRXU)O14vsZ^RVxp->_6Vh!_bDf|Pwjjcj%PD|h{is{jp^kMDIlSFRprB`#D&S%L#QdelXGX9kT<#SqTYsqF;eI>A4vAKyo zDqqd&D19uueipQ}o;fCJ1b$>0@JsM-c^6k{{8DH=mQ;!FYH13uL;N?T9u+u57j@6B zu8qamP4F{;4`i-qjD@$Szj?k^?Ra(pcocopihj#Ed(9y9$(z;SO+nT?Wemu4=>DW{ zHCbC3)b8x*pG2*Hba>7#r+6TKzdf*?{qC!BvX0jW&w^-*L@9?hb9BuqPcHq8fGDC57lIu@&JqMkP6ASX9 z4|?yf9RmM$>Iol1SNE6C960+w|9!voNF_vB|7B&vHbb6T6YXat&n&4_uFaLiF=J z^aFlnzO&q;bhLuoYUT{q`LDnzec0c-zWT10mJHt8_&2;y`kL38e%ifJ<=g(o?g8F+ za!8*9aY)^|?&84NdeY!HIg0JI7Z!`x-3KhLz}N^cv%hiS@1qQ*4;tGDtgmrMdz3I5 zTgAAzk9RMgXM1GK%XnyHJt%?So`xUAKImBDGh5Zn-KnfW8Tjl#U)neiZ;RmI7QQuh0g8%M1@`)?iiPD^_h1UL**onQv58t&+AH9PIs9Qoh1RYIvBU+f=0; zMS@4c4d4HN?7e$@ROPk!z4uHmdnO?ysilQ>shyFZPO03;!?&_7|$GAVLkNUo4xh&?lN@AYWW|G zbA5j52%m=RkhRMLH$VC>drgVxD>^k0cdkJH1*8=dG&wcn6 zpKA(w?$c4kdx7QHf7!d(CK6w##>Kf=(2mfNLE{Pj-|>8^l5iRx=x#%RN9+2C4k+yv zt}g+%)_dkWb~0|LHPY8zUqjvxNIw;Vs{kLpe9E3+{+4H1&+S^1v7YVVvlL#bSlTe5WNF2ODr}Qd zd<5&^ul0NfuW$GLQrCw%<{-3R@tC)@mN_Rv|8w3E8cFo;Y;WJWeldL_BaA+~+7Q8T zmjwDqppS3reI)pI(8m(`Fl1jRoqt)FuKJ2FnfMak6B>{?3Vb6i@LBP7E%Yg}L-Z!8 z6XFclWvsHSGn>$3jPm!2;X%7=YI>&)L0I_^tfw-9G+#8yqler&|1OSLDQP8waa z_N_kDPGOzvY@Eqcl-4lr zuWD_T#p^dt{J3gq)mp8W;!FN?@oHtZ9y2R$ecN8+Cbvxdnte;i=l(XjAu(T(nd^aL zUjU9~8*oq;;SezB`wBh`2RN7mjq%PEa%aeq1P^70k1otH5yqaIXNjyifjB&b|OQbZl_Dpm;MaA_fmgL>0#~aY{{oT9bL8dEbDU?-Kd53&9gYm z0nJ-CvYVqgG~L-v>~i(D0WB!`J6Sh9Ydd$ z^d)+Xjs8l7?ue&Lzq(FvA#(isKupq#<6yl=^sF89RY5<{)>PIsEx_F(D7&Ro6y>?^>GvZhZ*-%at3b!PxpXVVl2hxy;sb8z$9@9>D%PTo??oj zaPT|$eu6we1hv^m`5mF-W7z)jTT%=oL%}e*G=f__=2{+ zR{TEs74a+kve=`7vj$*Ps9RQr>_}BK{T&M$Jdmg=ugd;#Cu3AB#UH06R%^9Nz3Ak% zBaApmCb&Zn-|nd#?b9^<-1oVv?|SR|BKxf}RmM~Bt*+)dT`s1L@oCeQ`o6?@QXjO{ zmmnkGLdSTE?@%#D8FLxWNT-u}wJ)$d3i!Rw10uMg`2S4Rw)hT6+uhQ3z^s^$U zuPv8{B{&$HJ#&ZnGP3Yx$eAY@qm+AQ-re_EGs(q!ifhX9yn45idi+U#hlE4q0<b2&>0KjRMT}?&VHir#n7g84-PrE zuhC~AaE99VoVDa#f~%?MI1(RARh_5rs$9C3+5q}~EOJRB{*{Gphpz04LZ`b$je4~m zdOS1g@wNL{yENU8W9-#`Wvyhdmiso&cZiG^o39EPu%7r0a$(24==Y+BtMr${zM5+9 ziw&`6?N0hK_xx7^{m4Bzt8|uUg8OQ2Yv0K^U}ATIyY4c0MS4Ct#7Wg2^pO=EISt+0 zg~E%(Z=~LY#1-T5TRk4o3j`NQ;OiB1f!q0zpb_MckXKT3OPca~?QR$WhXatODh%n1-B(m&VgtZS6ZLzDX;kxOYIIh zb0d0gCNR;DoIP2k%^au826cqudkL5;!o1s-M(1ryk5;$2!<22ZpVmSzD;wI^{BV@& zli%8d-uwE+h22=OkF(|UXTCG4{T0pwZ z`V8}w%8Cg}WhI}b6YA)1FZ*oig@pQ&S>gJ54~c=>D=LbsYR|7(a-na16>Zkj#xbU% zxO8iB{gPP`xdu&kt<`MiQt!du3C5%14P&0Nx7O`qADX`nnYYB>)t5E3p7DaNb>CYR ze_|bSlKupb4yANg88wYQCEp|gT)oHHycu$CZOe*pMbY1vk#)sZZ ze-AFP+e&}W{AXRA+swWzeXg+4r>%8-@ct_NoKBy?&y`-F#tmaQ(!`gLSPk9jXVRsg zmbUhBR+yv@YriMzI0teO{m`TthxuidypDx^Z zWBp#_Nd@?Q;bEn9;m@g)1zzr5OAf28@2kl??}&si+7=z9n8Z$JTRwHs9H*yLu2}ot zhl`$%QqHdyxt~is`zdgj*}mv#74=`-|5|i(e|A`nBO2dIbiZomEFOl_j+iiuC<#s$o#?dj0U9g~(7^4$C3L*4?);jt+2Q2x z8fWa?d2W}=M`X90yUcjGO+SZNChG~U1)m{k$@tyvBA2?Zv+P4%`9s?Mfc5TKRv70^ z1zueb48VIR0B+gj>ZlvzW4?kOSRP^ZPA>JR_K3#lN_DW=MaXIz>wVzHuS`U8Z z9DXLX0g|KegRzhN+RruGyuv(QQCfrOGRZ#?Je+0C>!9VN0Kb@HUrU{dI-OtoR@cEV zb?{3SXIahu9AT~FW$lbJSzYIC$_2r1F=NOa;y-KODRzSS{FL+U>U;20f$db7=YpLIPVE$flXdTiV(x_&?KlWmr+Z$rJ3_2kon_5DUDm8S&zUs{Wli2@9KprWvt8GX zGu-3;8d$f#(k6J_>MpWw@@(+B{hU6ezpnd-p{uAR;Xd1D!C$=pzTbg6&Lhh20)AHo z@Kyzkr4jfN&=(pxb7kG5zo^f@(@$fd&s5f9{^MQym9hG9o%3{Gy6-0~eb@8*>EB-H z0<-kJiR+>ImiJD4cAFjlLHDzQ=6U#@WZOO9a}6|iCLO$OqUOAlvn71Xnm#j@cjERH z{m;951#}^PfaF?gB}P>gUxDrZExY%e)PSG{h0kpFmd$ZG9Xgl3GGde(rSwcr12VB- zs=Hdd(z{zxlbsdphf<3`rLW1Ch1Dqhm-c;W->-CBj?}o*kS{HqscX4Wt#8pd!?Vxr zYspaROEl%MlAP&lUPin$-gsxPO55eg9??}a?1x#j$**P;9)!U&n>wWGRsNa{I7u^m6hF-lmX9Mg}e6K*45_#oOL&?v0|A$+< zpN0JFwn-i5Av2N7j-OxyA_ru@kbOVcA7sV}3cUG+W!FAcT()QX0&S6_zm``}D0-8Y zxIV=(C~r44ROVM>Q(dcS&N)v~(*nQHKiHE+HtnU|2M1Q9Y*S;Rw(+ilM`-It7CgIX z*t(*!xw~G&7qN}=bg61gx!4!adWNlQzy|9*P3hCF+R9a~9T)k3{`R9~GT*dgDa0O} zsoBZ`H+#@N_{-`KG56+skdYIvIO$>8!ln z8|6G@gW@ZFUXk;Z+tXHs>2&$`pPgUR#PhPQ-#j2b2hNA{T`D}jkM{7m@Qe5dpQFBx z_)%mGBgaPe?0Elak*yJ4&dYVmH4{$RAIsPu$FdIpgciH&n6{2O6Ari6;|$lLW;h(> z(ddkh`%-UQ2~K1!(x}HMdmgr`__}xdi;Y8YcShpzF z;ThIpuIg}BgTrXn?0x*8vS#i2npLHIz?!|rnz3&4z3|zeSf|&rFI#sQ9=(%wn@DX9 zS+hp2#cJN&v5Yx`YqDmEf2gQ`MeSbmLi94L&Z*m1jNO=>xz2p1N@8_-P08-hca@v? zQeX0(TXK*%Eqa@7A3;09FJeC&VC{qkmTV1iADox)ObI$Eg&b?jpxY%+D(70zIox@i zX`)6+DZjOF;ziVIk$9ZgKg1Y5u0>bNDl6UPZ*Lznu`F}f=d4>IeU!kHcKA;09858E(UweCBpt$*4KyqUHTB%k8Lqvj|+mmDXa7RxH7`1QD!}3*P_$B=3fCmGvQUS zb2aSTmPqAXQwDL+8sru}<4f^3$sTE*%fmi={ek^$eM9*{Oxcta#yRFN#Urt*@toI9 zhL)Nazp<{}uAOdX&uuxFR_&4X%Tu0vj~r-XmRZ6tZgu@%o@L(2#Ar3I;ycEk(-P5F zZ##^&mU=Zy($tTaLHF>hz)v;fA$6D7Is33ZN+a<1a~?Q~ z^Wz%4;h@hs@a!CTHVS&{HNms4gc_2y*vXd>H!g=a%HfT2e#0ZaR5j_eoA`9pBFDOC z=9KT^eF0;lZgQ-<{Za3(N3T`-{37q#!bcW4`#+L5t#Cc_egxj1i+uPKJRJp}`qbXe zdGP;7)I}2BXV0Ji@qx!u;*_|kk<@#N1-1tA!rY6!kB|H}&$=|mpZR(F7;5I|ysrXZ zI=r8cyxtp-*AszFV0M$UAuu0@KNjL&tIbzluUp!cuj*%AoUelYXWxJpgRhAUvC4%y zIa3$@?c4%n)UVJT?y^mwj-a8dRoEsTM^5OqkSoDkE%%k$Z(xrba)Z4Bzme=Iov~>8 zJFGR0^(oM{qhW83gZCgw%xEa|)5sVKI*N}7%@@(yx?@gQe;gU=9)Pe6RJl3yB+FBux{W7@R6*rWC_2dM=id(#N^ zCSAuNrovd*E%E-3H&gG$(q?F&O*8TEb~~{eKi5OAEk#gEJW_auC@B>Mk ziXho9V~G!tw#9zht|0y*>k-hEu$eo3t>!u9uDX9$zpb=cx4CCq+0=W5CNzA8*i1W= zv2q?E(SKxKn_in#&mW3Vc8+D<;9$4Npd@&)YPfedb#CVmgBGztv4(FW5;Z?&^WLT>1 zd*8vef6dxA7xN8BEWzQP;&D=QFlv!4o--i4JD@rx`WKVL=U#sEKI(a}zciC0Qr4D# zG!-AZR#~~0d#4@v(xA1E&)Qe4eT#P@G&;SXHKI2Q&x#+afA9R(f5(T@kNL_tPi$0t zSMXbE?N+nyqlr;aOFXU&`dC6g4*JQ2Hf9hDy?=H@?o8@P3XM_Zv*vT^*bg8tX9<3| z+3a-_?MdY;q16ZNx7IhBJQ|2A)m~^HQ#$O{deNzzjN8n!Wv_bARX;z)!~S-I(BXUR z-_*04TJ3juR;Ek7uH)5}>AVZ+WVo+yD;>sQg|(3?{3e>hSmGMzk7RcsP>1tx6}L6 zbIpu6jhA;3^kxkNzxv*`FUQ9|=WBkN96HvgIUYY<@cx3#tU;bhK2am~win-+hTM?2 zw%A`X&zj$IpZpec9dzFyZKY4i@u}wyo)%8CKJiZ&zq(41J63TmM-8E;-Lqhfa8L+cMF=9r;(vzVzfc z;*9KJ6WPPYiw}VPYbtn@cU!cue`(-xMquxn64<+vks+yet^7Uo=_}Z)=CFTx*uSQf zmF+4c*3r&8w~W0j#jz<>@MSBH)%?;;%| zxkboshs1UQH3O}+W`r*Vmm=FT?bMB7TsJ&_DR#*i_JQ$sWq0Y;DIOVf<+Hcei;VJw zuShA~>hP4p_q-3Rx)k{&=NLz_^^J6zs#FSukc&|@yT&ig)oC=*A-Rsw5uaauUJ!KfQ%rHNp> zsz;gt26McgXyPpW*I3$x!pZ&)7&`aKV4Mg)Ag|_+YgnJs6JFk=pB>=;g+i%a)`^#= zzXD!<)&Wyj{DtyKh+Iuf_MQ_vZ!EHCN5I~#!uCyi|G}f^_kBcRJ74t@zjUJuRR&NsVOTx%-A-A0q2+)8XrCubB0D z{!hbreE`OP>jB28L&EfPB2)Ob^1k3vv5`;7^QHJE*oO~$&<7mkN?2u$=*f>HG}ip|`jlkga2!p0CfIkB3T&qD=o5SZEfLD= zS?_ySw=kB}Lq5RT7UG|oil4|47gZgNk4WAPxgHn-$Cldt%QgOGzlp3G8%c1*nu9i~R!nY0u7`;~?M z5}-d~DmwkO+fsMYhfaULwk1?I+X8*yo9U##GZ&%1?)3JbgG2jedeT8M(LWNgPwOnU zFzYG%`TS?lv&?g=hE1#)HZeNN#DHGm39^Zu=pVW06;(nn=oJq1ihAr~VnpS6@KADG zziN%W-n5DTQ7$%d`B@zQQ6Hjmu5`dCbPr1Wq-)SN{IXv|yS*A!(p`0bO49dZxw zA3Ks(Eq()d?uK^edX`v|=tOgv`z@*?s%7zmkH0dfY}XX(Z^||J@CIy0XS>Z=?tMM) zeyQa?1OMee;BUG1-plJ#CMtc)PxJo~HOe`SnrNIAaXR^y>ypFd#s}Jp4|FB#xDtMU zrB8D8p*~u57WkToKC1BTly8X{=<@qhoqsBUi+OaM1wF)b*ipNM4y4~#;nm-BZ7R=` zD|Al!jN*Gm;Qj&buVBo2bXB=0<6H`juIKqsYtU8Bs{U`Y#-&$x&r#DRqqi1@xL&~Z zq7c`oiC){eZ+9IBd5%x#Qs6FO%u8wq%$L$mXwrnMyu7XNJJeEtn_LU?9oEG5@J(ux z=bX^KEd2GBjJP|_&+9`jPdvFi3G6qC)D92#Nq&aZca`{_?&lLTh_m|akk>i z_g`3Z2)?fh@{hd&%{KGC>O=qZwl@Du{JX|^F$cLr@>_gd6Zx%?AM~G>Us&^wz_dwn z=ZIT!Pi*$L+4J9r|C+cibBbZz8ts3Y(7vR1eaX_Q3C+v!H!YL3;j>ZMBhO#1x4A?- zG0r;RuUq!-tq$hqVQwDwPVp70>@#f-+yp+RgO?k5?_F_yOU4a%fG_==``v@WYT6G7 z?&9w_b4vp4z}W*y)CLXrb@7dM!P$UTsX2Xp{e?A8fG@qaHuHP{7}{qmx%ggkSAz>Z zr;v7H&tJp*HQtL|0{>No!1D^hGycOW!Sh|VR?$hD>E{#XChH?U;dizgc$V>wKSNE~ z48`~6x0ITDO}uCD9#rm~E7yZ%^kA7|kHad7{*1^2W) zEbR{PJ|_6K5kKQ4(JJF!p|8QWyRCs?>)#A+Z(cUFT1Lb zcaK|hyXx?L^i-qSou+iUn`_omBl@1CaK-aQKarAa;H zOllr~6sV8PJBXn1>BjzH`0!dXCP8OL%n{$~o~HPopKbiJ7YSYR zJ^;H-r%Tym?>@s|WmqL5Zv<-e^tr_=cSB?zc79 zK6pQob+eyi-a1Y?W&ADZ`N1}$sY?e4~;d zZ?*Loe|l+n+iJnvd4B&-6e#9K9Z6S`>sq-Xq9W*BGLWZf6@doO3r z_KO_W_2I5GC;MHMEwNhB&m6}47t$V@o!_XRVd&a6RBdHVV$}h?L~2)8l3QEHc{*y8 zTQUisAS1yi9JdGGOM5Bk?V(l-0(qYp*(-xfY#RIj$woQZ>! z`Ay_CWMZF=Q8oSCuUQi%61b%1HM+uKtFEw5=pCEV)IH7kYI!<))Fg=?+mOSZInpA7 z9qh}&aQ}lk*EKgNz5)&1JxsabeY-6yiZgT{oU%Rt>{Czu^E%)j@!)IgVtId9+{3;5 zkBk^Fssg(!Zkm!ytn_taNIu~IxXB*tKZL&j2=_K(8(bTv#O-yEuZB)@6aSyQmfSza zlJ`~MzvW#c+N;VGbOp|D90TTk`D0&|b8eNuQe*2p`}{KZLd{?HX=DDKdB@CK->0(# z2lnpu#)Idsus1(U+)T}JyWH%FTlp>T4PZZN+b?k)Z<IQS|kH1(@iac3Tq~yHI^{7gc4)fYOXLOELZi)P?le3+11AKKn z8+=b?f0me^S*sp;`pEf^N9gJ!Ez#qvA3)6F8DdQAlf;j9ZIhMfR7?__4zX|&+`U&2z%;%)P;CoxJ|Gz5C*Zw!bJoZb%{6k>w$p<0$Chn)> zTk7S+K!c6oU-o7V{MT`Ar5^m(1@NDN?@I8W0t{j+3SB2c*K#ho9hx?3c^GH$UZ73; zoUu8wHur$bOI?#Tx49;rc`T-nrTf}+Z!_Ub%=d`k@)G9zcVPOW`3g;bmHF=bn)6k@ zV7}@4`r7;jtYfmpIReuQ<m{Zi)JKIhBR z{VN^w=q{^Fe0T0O-P>qiK=)sD-u0I_@5x_r-ql}o-oGcFd9n4rRDPd#iSu-R#d+TF zHRrkNziFOhFL9oSY4=s=xbN4T=grJBSpFy?yO1;R!79;lFD7r!y1L7QuhZYj5dE38 zFErpv6WGW#tU&jb_X9bIYfK~ud?x=-;Qvs_5xpOr0Dr)KHQrn0*6m*7w_yW!f1g#L zFWvW?FZs0ebz6wOtj}Bfwa#1Yj67oSVvC3l_cQvbVsDl)QWeK(?oAiFyMi_{j`)P8 zbs4KuzRGi&Jcs;j)NS(4dj>3>{|$IL|65@ayH{!uV((0sJs_}`t+MWAPWG~4;EI~- zyF`}|8EEV|Q+nKU?g_DGvQ{2_49Dpb*(2GzWd9QT!@Os`){{)x1q~+05AaoD*NILf zxGV%u4d_p@KZrh@NsjX?=);Es`tYW}9&$5q$vTKWoMh3RI&0e{`Tt3q_BrEnN||e_ zns8ct334Xk6V9g^*OGPk?*tR;Lo}T8~8KcGq+L4pRxaP-a_ik?3CE_r{GWC_2~v* zsY-H>w%)0MOLR`i*O9hp0#YOUbyo5Yr__O7gD zR(t+Y>0fX#!!hNwhdks=YP8AxUSn*TUqAe_GRGE~>$S=*p{uvBo8Fp}o0A!^ots$i zPtdimVs55f?BtDt&s3+xt2+HJtuylK&3yD<xuq*>Guh8L}r5HOyqH@+OJ$2`BX|0F@Eu*NS%g$^t%@x z6Bvb$8qTzfA3)z*#1G&-L%tDy3acMr!>6I&Rrv1%!*tpbTKgi}x*phM?KQPub!wWD zE5G}Z!`ah11BbNbSShsvD~0T@vok>r}*Wa(gq!+^eCf#OW#5}QWL_kyXCB{VRz4>&$qBY5&54Yvd^xbo<6#A?G(Gcx^Rj2h^MXODE8RLH^(hodrzY5tv0_^ro3rO zsjjl8{NCo6^cnv4=>{Kl;{VS!?HMn6L<{?_$g}q>Hr&a0!yk1IcoUf>Ya;SZ_BX>9 zI6eoscGpo;Nn=i}tV03$Q2ZaG;TJdes5HivcW}2NUyeb80)xsLhp81$6CSiDRXaIalE<^@Y639@&HqWDoveRJFWI9~PP==B;MZuO z_0IP>x6YUZ-t);v;LPX&)i)k`6}V)7nGz(I8v=4US>_s$cW)q_6K-tmY@;qX(z=b?{QpzTMNb7knkN7Bi@4R*yVXSwpvyRGrT|aeTm`@ZVg|mHbaF zpP=*C0G+>jUJK!k^v~MYi0?hryJDNbxA58SwY)PLdJhjXRFL5jL%eJpbVNad!;cVnTfaeKh+%V$8PGZQ968qcM6z407_O5=mY;Mkv zsT29ZKqa^6!LS?2H+^05wr5Nb-jDG&>#<|4I$hN|V#m+0kKM!nH3Qi@=$m@&Q#}sy zR8=Q2WXG#YKO=rTS#S}+(^AIYWV5l~8FJ#E_aU2PXrR;(4AEbCsU zr2`*9w4xBh{Q>%#8Aq1iW*m7VIMCzBvyC|N6ktsxj(jGAwd2#ob)ofMTt7rULJQaI z%)oxaIGe*PWyuo*Jf z(MqlvYwoy@-TXK1pP#n}J*)E_=t+ede#bTXsfQOVB^$sPwa!W&porEvIY9Ac!*QpDMFDR6K zFb^2SK9IhCjV|M*9zYy)qsQof;qg6o*j~eVU(PHWw!?2a^qrn!sa?g2a`@lI|8T#{WtDOzMuGBR>h?$MBm@|EwqcWL^Y6**)OrEcFGx z3}0q3#>LM0i+$K1Iw-|HbYQQLlY4pqHGW@=G<+Yzk0wu@!Dd*9J1-A7iJs7sVIv>k&OX^$<6q7g2pu64#_$fM`J!)V>JS%naW0Sir?d%K$eVb- z%VRbuwoe$(YSk!Aa%c|RfiD$g%_*t^S=aYA!j!GW}!My_s8aGf6Zn>$)ERBEwy zf49NEIQMZTaE3$nJh8=FUYqoylU#UJBOhLo9CvTFlH+knu6uq?7WwHiKf8b9koGmm z*D(zd-t2ugWsl-bb2UV8#sXSUB*&e(-AbFPVshz`;Z>u&=PFL9zDnwVEqz|`Rq)(H z#d*0W@gYlHl6S$coI@XoJkFz*U)6Bp9rzO~@F`c=ynAXHV{e4AyH08a?Vi-e*lp5x zK^t{P;jOBCwH4mw+tyl3-)@iFmFG!wRa~v?sVr3X$k=k0qk{4E`?=iDO>;fTwa1Iq zTsQls_^%Yct+#!_(tX4 zG=8hB4R!PPxX~GZhb}@+^d4KZcTb5s-E|fJe+SN9a)LMaAlL7RDIARO?s+-JyQjfD z)U|PPnrnYehO4T^?P}hf?s^sZX8QDYY`(B23q0HntVaH1g8xJE_^CB;ZXt7+wLtN` zMgH{iKI_+>%rIzJ&TJWQ8|UuF?>5@cJi&8CjhlK10-sJF zdHb$xT&8T(K6?6yW^4FobARM6V?Ce1*_ptY;v<)LbT0q3(O(^XNR7I!d}BlPDJ`a6 z7yXq3M@fgJ&Yy1 z^BBKNnAanGlN-N#ysC^~WRAYeKKL@L&8~cSkEVCBr56u&@7+ z{2s-*yLTfmFWDb!{Ho^t6-OraL#L`K118W8^j2CwdoTEAXB>EWjFPB^z>^+UhX`$r`WX_4+$n(r}S6XI3+4fRAf5!K7>a^*P}k{M^+z(*n^?Y{robCI&B$yIFHq7 zqyCHf`%)7;JvuLYXtFoEIA^HquACvRK?#$xM|$(KD{@A3O5|%! z@}WsD>pn#EIob(anSu5K7ru`y*%Q*ZUw|%Kg|1V|cPZbdFSjM*fWEhA(6y}HtAjW% zOzndb)=T!)nKlJE$o{DBs}kEY>}+{%$)NVWf|Ih(+sBHJK>TN-o_=XNFV3=2kq-&|e^nudH#Pj}tU{c7&#PaEM{ z*5G!1kN!)*-?C{L&`pNxgIm_8{F3|9zVTS?;=JP<7Au@@a8Gk{ADnalH8m<#QivB+u%Q>P@^_(+u&DIg7W8SngI#yC zKA&WcaXqaKcKxR|#C0ljtg9wvsH=)KO+@}L-8$CQ0?%LX9Oar1kH%&RZm8{#-OXYp zJb%R)*In8u*AvmZ`I8@x9}~p;5hXMq;C8{=zcc1 zTnBJg8igd9<99Gy2N>%($wx1q%M39_U)teT`jx z$hxf(@FPEYs(nm@Q;7@3LtcElS0$47aoul;*BU+@B`hynWP}YFkpX>7=6ku8;SxE} zeDC^{0)eeBJkGZ%k7cg5==^LiGIA#LWR;aIJR^7wmd(h^Ed0H5YM~|Jack}YXOOb$ zu*-#)t=CQ7y$N4aqns;deI~Ld(oV*ZwK&MfShsk6-EOVty&OUPc!U1`e{KGW%wJ)j zmwC2t=sJJV(L`2M1n}^FE%T31vcCuiN*HU-zC0PZ^!;NS7FQtCH%}Yn3P&!=@8&<5nN2`#d&BwMbF!fQ9(T(Vndfvk%JA1Fj-s>Re!HJI{3Lc8) zY_%QVrBSn7&kJjq)GE1$%|j#V3*`G7oL_=Z+^mtx8S${Y`Ia^NLz=7c0c`4J*kZF) zUw?dVsnN>(3iL~nVYSGp3gY!ACXrK-Vbnkn*%Ss&ZTJjo!Lj)$ee`tknbFGb_Huj?^e?)j=onW+N4otJct+ZWYBzy)GqEpAu`?T^uFXCf zUzA-Ua{xz6!J+OaKo|Wz)B8XD6w?L+G(Bd_`?J z=(M8GmIdjuZ4XRi&8D(;h2*VF!R{}(qkWRyh_l;mtZf*+qj1`g$MOK@<`1Yo$&ryf zC-I+fKCwpD1zU7F?|Yn|hwf3emUlOxd#^;#7Cq3du3TFZk(WIyCO`XX?5rH%topfL z?>24)wo~du%KTwHK9`^UAK3oGc%~%cs%#tkf);xjw3_K^jEcx^`i8Qn{Q>1Zp-Jyc zX|8vmd)DV%SqwCWPo6WKvb+{*4C0H^hs5Kjy#!wSnD=RY8D1-gZ@w(AHR22U{|&F% zyVohV%C3ULUVJ8Po3&%GUh|?ze!=+wNl%Xf9$Svx+=sL5+ z4r-m=whcCKn{sSJ3_g@L@#8k~{7C3n+Q}J+Lw#!2%AThTNOM)L#Ygi-+IMZ}yeH$m z*|X5!?W%XrEPTH+xmJlDNsMxjT%VPK&l&kw;!^gA94b9R{w%N;eAYg;WK?%J|BBxK zW#NqcvT*jXz?l?)Gd%$3kN}*=mE2IcZC@t)OE|hi1p7}Udki&M9&iv-wXw&9vA-C# zmLygoxs&1xMlK#UePz^AI@bzr4Y@9{(M0iY9yzeSbKc;tV|`SyZ}z!e9lkYm4+lIh zzDDI(-D2zYdSpq{B-tMSpCJ%)i5ZtX5xF+Z(kp1rwrUrYJ*P@*Wz^ri^2K z+w}WU%AT`4tBKr#9-5#Bsc+pF!|#XyZP2C}*fn)A-f^XuLVp_lIRgFFDfB1pEwt1S zprxO?J84Oaf3ky?ROn_u^fU>2+DO|lU6&i=S_&;~#uu2uf6?=nK~KLT*G_CK{49G) zGO^FO);v`AFC4b|wS>Og9+<+~6tG6upd(z3j&Kz*AY(r}8y8lSrg+=ZmApIb(EOlS zZ`(u1HpXn#YGZ!z=8)kphegM{eC+9%clk84NAF7*;^O^d(@WkQJY0EW#A2~mn$e3h z-%xU8-xQs4BEN^w$LsX5{mqfX`$R;?#2$Y>WGUHU{2-kMT8qDwTtii5#k_YRyb0llNar02uE5Py+cIfYb zbqmid&m8O$oy>M&=kXaUTnTcKWm>pi@1b00OPyZvM;Y%I%E5)3a4x;76`s#b>+d^KT9vzJTBHt2fOUi4K3) zn;F9=@q2bwhASf_!&M4y;(&A3=8>)`B4=o?qSK2EsNkO18EPys5$M>|$A{v>Xj)Y{ z}v&`)y=y1)Q_EUXG{B4OM9oSTGrcr%pKzn zQ`*e=_Y_zB7d$I^zge@k3%{a6Dd=vJ$FSmOZEH3Xn_1S!tLxAr=L9AlHs+UyEh6)J zh4}S8bZ&|5G)Vm^Y_K8I26JzSD`MIZ*EQHGjr^X>S&v%U3oU7j79Bk}r}V`JaO9vi zMrK~sTKRt)^O3gyKu>BW9uRx2_eFWX%Qg4|rC#TdUU@Gzx<|M^#&6Z&9_f00+DKQi ztUo@l$=DxC`cPK^eZ|3N?sWJ*@LP0$FYPZwzAU{u4}WfkYvdJ!TsA)W&UDun{%_In zZGiiCu;H%4za&2AG_Gx6-Y;=&vU3n?gWbio3T%oDa4<>!Ge23kH2zCGXA3pGMF-di zty3>tk3EhH;FA4gPgv{(*KA;(-Qeas!xhFl8nzvL|3$dQkEB4~Zs>oe&3lIiy`n?6 zVe_=9{8s*!IXwN%wBh!gr7_A6m&TMp6E^5qd_eEB_mMx;CVSmIspxj9GG1s!Y!a{6 z?aK6)xa8hE?s?lI$2CSG2mj_Ct|A|lV`(wE9C#VqnRi(Qd^BC;P^GSmb>;$CZ)v4*UeS&FQ^oc6`5wa)OiT$VB8oCWv{AU>{Frgf<^!M-Yj#EGi(>h5g18-RvkdzX(&3t?0jOaOS`roVzy-;w!);_ z4Zu`Mt@)DSUVT5Zk>_!ebu#TF?CK*)Jl|mb_u^%+%;L{(vUF;)cpes07-jJ@G9T-h!x*!c7>?jb{DY+({=v>zm3h`dbT8| zvar`r4|9^!7DY|sXwJIz!VZhU9_x)=7K_ashs_+O*CrNUJ9^1>(?+gamfYz(d^%G1 zy_bpq{b{Yf@gzR>W%$AI7hg^dN8z~==vwk7Yi-I{tIzGav*L5RReWySt@dFh{NDD! z4Zv|d@LUI6MZkA0XYrUk5kjZn;GvnOh$9hQF6t8=lUM~ zJIwf2dH13GSH~FN?UN$$`y@fjeW8_PXr>>ulL8I(hn5ErJBrj}43a-4v=~g!ibAd? z>!G2W)!OZ^UwG=mnjQAU)+M$C{an1zK>0d7r>(edSz?ZshOYpeKFhkve1F9==5-XA|a>Q@EahdJ)lgk7@3I5xD&R&Iou+DySj^xao%<%3ue9Ll%je5KIx~s16 z<~H}y#+CA|sj0=KS17rUGfo}<>jJUaZ2A%2()B9({p-&z=rTdZ_{~o*=rty6Jfi{I z#!=qfP2;?|8&-O=TY;yMkL2x%4~iJX?zgd1-Q?Vo>!)95?TgPpe_@T}$R%x3ecDOY zC-Ue;=J8a9;#)h3dfu}Y-!9fHd2gCe>S!2!_G1ny=<@yLY}1-GJTHB2SY~_uEd5#Y zw^oq9a-)erw$oXW?oi2R|oe3QGz)$VZD7!2EK-GlyQ}$e`qakfh(03DO zTCW85@wAn^9V;BaA)ogauFJczB=<@|_qN`D7x{EA_y5fOI&dVo6*w|=eS~-XanFgr zyAfI2^t5+(Be}y7{4c?eCgTzB>D$gbOi%P(pCbLHajg(|CD*(0K*9OO-q@~OZvt1s zPx2l)YkTSc-$Y)p?oISPbS*W~S>sgVxRNi`(i>Qa*W*tfFY9_e>uTySrcC;X{7s>k zDy1vEG&8P}X7l|XIu^JO$v6IqgM8O5yD6uNIc>1t)GD;@U|jj$tKaJ`k7w$3^0(oo ze}|{O1#f*59=jEJd<*jUW@3Ahx-71e8kgjZ*0J7-+L%~eQD`LaKxyRvhKxr%ZxbCu*~x+?jIO=t6XTy|vmiQcyCI`~fZ zCoi&9;+Boro>dXb*vxipNglxW@gb3;KmU8Z+^)gVm$}~VYtJ5} z|ITt<9X-q?_ulRsmc6`JhU=cKw;Ufkd(Psb5>3EM~f!jJDIkcvd1I!^{!5@gboPCrKln&!WhTGH zF3{;3nv?yefd9&tDn0)u6g^5?_MR>@sy`1657NB%r@?bV+YX^&Wqa{NVv<@Mv6TCi z4jS#LU-5~`*_z<`-I~CT%q~Noh|fTB)I@h{v@5>5v7I-hc<+ zQ@&Z!DzKN3i}&LC3w=)y`w#s*S1`?)u_mE2FKyGw*DSzS>%h;TWM&dG@fbLK_vr;r z#iY8JVyaxO*g1=G{%VZXb5W9@~$Eg3ID_bR%T4LgO} z^#>F^$4z2gVs{q|@@Bh%L*X2joF7Z$ESAa#zs3%sqX_i=1~YFKT}E$%UL$SfzBNYP zlJU~*ij$8GL4%`~?yU=IkKD9@1E-suo+5HlG z7ryXvCT>}*H`}g}3rjpxVoVZSDuu4+AWy!>|K$zj3W{$*>MwA9WpF@OD&W50=t}m| z<>W+4oZZx;$~b>wJ`+ZJjT%UDF9ILYHH;%?w|WJh*ZG09jOD(YINd8;Poq!Cv7?58 zYdN1SeBSP>W}iV$i2WG+OdWf;z&w+01(@^jgDKeWX_8MCNq#6eJYhL6BD(x|O&PNW zeO}^MO?*VB`fhUjnw_dLt`FnKf`>baGsnS~v69cdA3ZKT%_X{(^_d%Z#^^t<<2;J= zZ)fe~x_*u@@cTq>+DP0F`jG3NVMB&GqsKbx`n=4k5PWRV>1q%;hf22Kj(3$$PXj0X z*Uu|TzPHM}z06y#G3Px>zY&ZkX-BL1nC zj9%n%boWUK{Xl!GI{i`oTfO89RPs^qk(xOG`k5;7Y0VrA+h)$QKpO^+tNsG$Y6YK` zO}2U`G$OgEUOt)p&fr^Q1{$#oam;AGB%10xqRBD;j&d3+ip!?VZf zZG={M-v)4sO$07F&(V=TD7Z28=vOs!trXw2{62dUJ^KK3v5B}D?cV0U)HSOjUqtM` zC}@C~Rn}Sj(kE9h$!?5ToPA>Hj2A`LJjz<1{67Hh z4Z69?LK6x8gU9Nq&5^8YkGP7R+xi`|(u zBMzS=ISEcheRW2j^0Az+&s?~8wfkLVUek~4y|ZFh6+2_MFR;B6HgDP6wt1)V*HCxH z|30#4bNIdq4~1pUdpK;wydQ<-%sV*1U3BoLTZ#^5&Wt&@d}hpR6KpYmnh+6lc#(hI zhkRb2;N)6NOzef*j*eXHU-xcM`w!c0JG!4|@_A>=kGS^^eT}5;7W&&lf19{Bn|t}^ zZ#z1h&uz+3*CzUxYq7SWt~$=KeFT`%do16urjv*SLOoAiwF?{Qo=tzq>j1^5p~4iz2U>IsDXsjG`^m zlY4K`uWc<_KHzCSTZ)#4S57GBU8ozVKPm4AQHVWwsIBGvbn#J5aaL#XIo-RW_%iT( z?N#1g&n`;7@t5C;z462BnEoH4M}4>`y8reowHsrH$Mlc=H>FSP;^_X*4OIFJ=l^Tm ze~tUw25Pj6yzxVHuGq!BZ;W*%_xatZ9=6Y%Cf)U_(*FfZC$l25c&VhNcyGkxvgw4Ju8qLFGtePeH!x zG7dT$%qN16NptcJC-GrOK7F%Y%koV2?#lX^)+cLV?C7lT#Ey1OR(27G`Z$mMMrgYk z87FHm3Q(zQ42#nU}RbaIR!8;ZH7JRiJTSSHr1MbU!yLxa`|8nF;`66fk z@2kWVh^1n~2SZ+MBF<(nj{AN$g3J*^`#Dhb;f;rlRHS zAIsT4R^#x?&R zrsMo?(UC6%9jyvMN2241{&FdFlo6n#8=w>OIiaJ$UqDAi7Ml4T^kCAJsqcIdT?rjo zX-McybeoOP(M!AY^zM< zoOb8a3zS}AMd}&*b&1T`%7^QPGlv&*?}oyea|aiu4foNC_@PDSK0{op zi#Z&T*#wgC9e1E)6dYsV&d4;)YCzvLaW?_(RN{)dpkTam#B!Rgb;;Zw*^ z!S8nDZmi2)^c?>M_JeP-7JMH8=U1N}GWXzs1w~uXqfc@D)SE;244QlDrOidBs?v&L z|1RsswOt$d9yB-h;O3&(m0bVL2JlAv=_9Ti!u^9hbC72a^2|YLdvoS>TLx^U->3O( zFZw<*un>K`iTrob#~a9F3f9MmK}S(SNBBYHT+1Vz(O5cVa+`cFo9S&E^saLG_j`rK z{DZka%(tnxzpQ4so<$xW=6gQ&`zdmq`f#@WS@iQ}{25Qv_SEE!*b0MUz{l`nSDNe8 zn}dg+TD7`2^BsQbrL8f{HHJA3*ZnS>hxO5q{`>xsu2YjY4Uf$qt>L7z@1%AIujR zxG(%FJnHUIK5-f~GUz2jPX~{H6x& z;Vtlp$rEDB9)urG32!XYZoCtEmUG*2$wOSfgf9-k7hC1p!04ib12STe8N)}y7YE-= zAAWF^uQxCcho58M=NR~T_$i(hzFMX87r%cv!gX-+mf@$ogUH#>b-e~(yboXeN#_f( ze`7lMVy4L#0h>Vd4tLnT2|elDrd@CvyTHBNdtTay)U{39z+F-7QEC@tC3tfR7diSD zqVt&c$;=S;$;SbkU>JIs4vY7#Y2EbxfNk=Oz~Fr=TH0Cdk`B8_!!8-){bAOXv7@Ij zuGk}??4tK9cF|OP5z^lv#%(}GZopr$6kRbG1}y+X6!R1q-sPD|@X#PW;e2GD>1HQk zYh(e_urb~o&v%kf5!szya6E4!oVLuAr6#(D(Ue^GWjycxYNA9&|1>YS?^ z97?_XmC#QnxX{SUa4QX0D&C3-bEuVEm^a!tMa|rW{X52S-sVY-HF7h)Oxy17<{&;> zLM_g=x2j$tC#sCxZBN@Vz7t3B|IXUn41N&J`omCxIE zXuN|$Q$N+;3tWl5r4jGY9Fhw+@%UlJHe!TE9jr6A81<%q&Gm1-U{A2t+s%}C%Z6dT zmXwNO&dAogC9bhK%Qxva_L?hSu-C}>JDr!{OATDHc6#1SX3&2bKa+ZJQG*3nJ>yLD zyhosyYUtXEJJFGonRhVG%=VAc{_EgQ=hp!2IZd(&xRohvXa> zb@N2NXsZ}2Tv^~|KOpx4(o_5L zf)A0u7JU3ouPdSFz8)!MtYCa(1n^N|!N=#{q&_bAF#B}SXBPM<1Rq)8gWNmbb5~L9 z;N2t@f{(n42~)vGIWlRE>e=-kGRfJ7Oo~&^apF*4_X_Y)0Y27)k5F(dh0ofTMe>fH ze)X+0Qpm0BPaXid$`1@A_g?aq75^cT(H(Uh`?t0)vDKS%t!3@+PVw$;pQN0lUdiq; z)LdMDwGpq97@x{H)wvIzm)f@HKH+{MG%|A|wU>FuhJ1Q^nUT*on|X!9SGPYzJ)`~9 z84XLG+Z5GsrP!yQ63Hx3vy~SKyCvwhL?8mqgSXwR;l$k^?!8Jk`ejwCi0V^-q0Gw7U!Xpq`nIyW{za zPx?NIFLp3MIjZaXfG=_b<*dPYwQ6mZ zZT;G0h#>zV zQm^CYp3IpXfkl^7g|weSzXiNw$>zIK_(9g2{j`jAn5bsX@uHWbL-)~hXr*2gafq3$ zso>kHx6OeLydH9kz-tEWG&R0jdyf5(cW$WU3)rsmzNqIxQU6c!chmTmc$~y6aA+n7 z{y0xHhP}NDeU;L00(y>>#;EypBncYRX~}NeMqMZ5u8~u}QN6Hc?~*I)Hz3nbKsQ29 z8y)1)*#9=6F+M;~w0-MV)-JGaHPF*u)@~VV*Be-70ZTz^r~WE1sK6k!IELpWe^ll~ z?!=zg}GRl>rGnxgWy<+I-I0kh`LgvZn1an9ct zbzSGK>!I&?dj8Zf-<09TzE4i-M@sT#zN>cGYbHWR!LnLIHfOR=$=YiQ?;uI;DxXVO zdwC~-$bx;G5eVLwo??EoF9pkNYu=r+)?4pT)#{17(G;=HEcL~2U^;|2B zebsp1YM&i;-FE1>-`>8{yeBwo$@qWBd-wRL%4`3B@0kQ<67J+iK$C!%OcX0B1Qf~y zyo8G&7wgep2zbshA=(z}9U&8({hB?`ey;0TYdveNXFcm_;v7o<_qHMXcNQZ9 z(K`+yf@R!{p=&L5tjA}cu@c^add-MT`4uG@JeK#=Nwt7 z3|(cG7kLnwQRSzj2N>qf5AA0B^7%QsrmHppeD+e8*G#*_Mf%PZ{L=LcPp@Qtc8&ED z{~;e=82p*QrP%XhvG=02jr--D8ZUvKpF4?Md}8*!lM7b8xY379@8}SU&uhu0|FY`I zlvNoUV|llSZ_@QVe4EIAyt5`e3VsN-(0FGS>+q=2{-*4$M(~>gd?rLI_hJj=~GUy(7| zH>;qg^at)xDd+DzW_nF&3ikyv_TA!1oMDxJYJd0eNhYblFJ~X~bP}JIM_jt!Bsr}4 z_41sE#+>OuM*c3an)0==!IR8EwPZTZ4pt|UW8ix_=OtNJGGt)QBghg}Q@H>&O>27# zv8r{4&SB;>7vCQF<1Yhlxxj~VHHCL5g@qrkcI41AM~pQj@mz6rnEHUPcW zb>0<&_#Zpg0&sOOdM#-Mmz0-JE(aFduqE8@Gn$UO2Zd9$w(z}Xi#zmfbVsk&A0Dj( zDiaI;*u1|zlQStyn+4^|p}CZe7A>Lod!hZcTarT6jHwzq)MIkLBe+}*Ea3;ktC2;8 zA0FbNzra{E_qto|8sHgou_g1Tn)-|V`e1peE%jTRz+1PU0Gnf%AJM2@+#+O?b zEhm}O{FJiqK<{tIjrADxb@p-EWfG0`opUT3-)D{WjlZ$1#{0_zWfR{uZO3Rdx#zVM zdh*=rw{%X~6V`7rg3ekXA6AF|%6|00^5v7m$T5~J?ZNU!$Bs68XpbFDHluDk8g`8J zW$-L|R>*2A~ZVH8K$G_$UZ zuag6&SH-5az^^o>N_;7;^rD{jZT>qleB;`#+HW&vwJm+h$~V}~C*4NnJukO-}269y}mXeS%qav>q3ADTdBk z>W-Bsq<6IvQ(yI}U&XS~I(6#qAkBAleCp#m>Rg8nNA*Y+U-xm2WqWnz{e9-#vA4<= zS`Cj-9j@qH+Gizt-<|F^a=7=_jZW<4jA07;Mk@NjAoPU6=oLfID-;9P!0*!&-dS;V zXmgdhf7TV85k;TVI*Adt|CS?e(!1FH~B9ftFxH< zh+VE2aAU?8BQ_1?91Q>ecz)L1yDS|gE&QM4<$QLyg!?|Ry`^P?Z}yqFwD4^cbavAS z-W^a34)M{n@NjUVb@l3l6>ek2aW}dhcOW1SPi%E(7Id=yy>h<)N%X`gq$kcQE3@q4 z(*3w|si5993jELD$*KXiA#C;gv_JX^au|BaLdm7gtWo|>+pfo2F~VcESvHo<+$kh^ z+p+CQZn4@~IyP8s7{g|jJv`aNTpFhCN|-s`(rfAS_kkbyu+X_b<`=BzEu5*_oMh}c zOdTyb_}^A(Y}ni3smeb#CVx{je;&3>ouiG-D;});Gh*^LIQ4Vq+P;4BFN?{)Ihx<> zh-`1Yl5?ylBinxpt+rl+-HP|tLhOnBYF>4}?dw-^=I2D@suuFvX<6ZYq#2~GMCNLt zoSmK?{srm&=+zifMX-;wKiMKaJ@DcX`2jq{Y z=Bho8z3KHt_NH#!uUa#;xvFYvWDdHZaE12TxF@%3W+bY^N`0&Ui9~s#9T%v*I5xK_L}Z^7TxJ=tu23Mgk{fRom}`wXn^sqxDnfh z=q$qB9yUE1&|~FP(TDIyI3YT5a6)o{*4c}h=g#(-+!g4^wi(C|>8!1z!tW3-aS!!7 zDPYz6~0?bq8ROZc|C)%s9=4lSF zpUoW1VlIlAryD{M_qohb5@%}?&6B%(o(?0YICJ&Y=v@5@y}9#K=PENgSEE$knya6I z<8NDYl^IiiI`cFtd@FU2typ!inR(GXr=4-HKi9eWNT5{${xZE>G1J7}eB&XQ?*q$Hhfug5H-E*Y7 z^`tARetV8SuFxE-qoGZ}Q2g#(X~aMm}kUw|w~7 zuG(tkNduX*g*dZ^NbfkUJtZUfJY}31vrb%D#g&M?OF;1>4nd35ZMCH}D{>97{KxUj z8fvXyRj!8SDt80_kr7zS_Eb*IhYz;i?wZm%(|k~ET{Yg81EoiYqwnh9Jl3=}+Tr(RlQ-=${1unR9{-GF-iCKgufI6w{Hq=d_PE9D{vC-jvT&egVO$)G}|6;$K>sX zmpd7H846Du#+o#owP^%$!ARtSBw{Ao>y2XetL+Hzh-`IF=@@4O_ZY^G6Ss4x9M4CA zGgdsHR=!12fo&?V#by^dY);X>RZfK`WXlO=1M#t^G*jML=LOIEmKp0abN!*nK%+@_ z@)mO!tzb8*ZRJ79Str++!6{YdkpMDs>sV|a*ka%2?is~c7H_m|WNG26*z>bsL_J3( zch1!LE@QhvA9j7>UVn_crjJqgabyO`7dwgjlZ&kk{c?onNB`AE-I_0T>{w&&CvH)6 zZ~cTNe`6=;C4V1N8>$nWs;7S9s#3J1V5XPJdkYd>9&pg3G0nXdf)pfBW}wi>~sYv>!}$`uad zTM(;*$ad_B(Lq|FN%?O!y93jT(GRrWCY>*7HTn|odAz^1iuY^$KleP*b2pvxdv2;< zXs~~we5aJBhCI3Uz22=eXR!C3eYfbP#f^VR7Uu!!-&<8WPin^wx{i0zoz?FuBlsKj zn>jd_e%Bv%?^nAm)K&c^bW`ODZYFL1?;^FoTjvk$hUX~le*VKdwhO+^^uwE!G-S%h zs}3fEFK>)Bf?4B?;FNL3kzL@?o0RdmIgiU8rEqiEdd74nvb~P@el6@t=22Gh-Hp?} z%bMpKU8=tYTu{AQ)9v*;4S9HEcqQ$&f?r?bu3F{0hW~Z+@9H<`EBJdZV-#E_G5_^d zw`u>c*ICaZ@Lc!@9$~BNiI1y0JWt(G#G3jamfe3acnv-x`wZorZeJcDT>-r7bxwB) zeCM0gA8Q+T;z3&a`Ak=k{lV=m$U=|8I|R#$)kdfS8|Fmb|5$ORHZ*S1_`~kB0-JA$ zCyVc8LhBj8M>38(JOCMJAkTNcWUP0}S3i`q$XsRLZw-%He>-!ceKV^(?QTKdfBOaY z46*q&lkVXz?6LxXXtwFyH@j=bQ%l$@+5*m*=;eaXkEugEsEjn(v+>jDY@U9O@Ta{u z7+-Y%wHN$N;1f^#ZR6&1--k{6KRV@JpI?XV+<oIT&HQ$Ln(xh>?D0AKNwK~+(0W4O8z=wY>w6>J%1eKVm2spQJDtWB@2`~(fBCJq z=g#Wg?4vonx}N!r=VbwH~oN)ha0?2EJL z>$;0vA;B`$W&1Iuy88NlIk1WKW6A_)TEWw8*w5yi$($rxK_Z_fr|jPgSNUU<0)w3{ z8?MgYs^jlhIb#%>1@{CFy#XWNN-Qh#Xx7U?f zGs~TBpYI)R?4N#wv()S(&30$)n{AHSx5PA$Y^C46t{Df}`wtehJ^bVxY;TL*sr%k= zrS6+kW$YhB-H%lo`#*H2gqNEBhTFXr4VA<)tMX27C^yahkMT4h$8m4_j_Hg^Yf#{| zB|qEsTF%clxzgJ=BM00!)(AZ)e#JO-c0Caf)qUezI^AjgeG~3C0+-CMb=AhxfaF#u zj$v++G1MT2K75uqCs$iM;cLmp`nnNDQ#I>DeTLDr^NbnVxy6|B!Viooc7J;N`>Cy5 z{5bT)@8{3*;u}&lDd@xhw~W{n*8i4C!T3Eg2d5}+$tQ~__q7?5ESbHP*d1SEEUoOL z8D}C_sm~?ZJ$)|CFgmZN&z>uYhlcNV$=HFuiq%OWPcHY7Y%w0R#Q{%%0T3z2MJg9?x)i-Y~`8@s>OOc)5JnShdeSb~tuaY|W?c_$e}! ziHzk!)*}AOO71gv>pm)bpQD&@cy>qqpFPGT>wo2>V0xZEB%h5#%>QqSQM1g2TI zL)*D|MhM#P%w1{(Kc>t!=J7+~vwq?3u3G8neQl6->?hg%cOE@&a`Wv*@OQ|N;CH8I z8oD3~Ns>(>(`}t&DS6bF>O<;>$Lhxy zl*YIsz#)FTN2#X-{I2>vdz;9%mHevi3Vyp*_UQH9_DXoEbMIwnB)r%l9*J4GL9{1d zGr?Z_!1x$j{6esG1HTf3{U5i9p57A=NjiE|mW0P$9`u0ktrbQnQelRGPw;D!*Z6H^ zoL~8+{rQJ9NQN@r@jq}bQaTzybD&lhrMH`Z5WcZA)e8=t8t^lfvG>1e2!;%gZP&R3cH zHD(|7@BZ+#ls5Jz8#~k%{9$|RP=BYHhad2j;4Nh*Q}!bGienGA##ChUyZqEBziXKk zjMWb%OX&QB7aHmAJG!j;{{Cl*eIL;G53iZ$3rG9DjlMU~_h;z)v-G`rreea{eHR`$ zx^O(s#p3IZu#K;x9gjsjHqQP?=~4K_mv?aX4fG@7>~HwhoF3p;_ie}PJ-eZ$u9Y`| zAM;rc<`EOdjxl}kqa891$OU!FjNp%{=RWw}V(@0YwN9pmF$}K{9JANQK+O7B&APW5{^+ca0oKP& z(e<%9W_?sX@t=*<^CWePrpyV%q+=Yl0>Z^rlKHyXpZbQ>Phm6pJ#om+Dru*OE7&G5FZqYZ0MRAZ~#y5xaazIHa*I#M-k{~=(}8NIu`1HDx- zk`&j>S}*u@?0I`1?y8j>eov~gy}~uB{8D^ibJ(xWgU6Jj*SajdR&dJ*zr_46fIej} zo6j%nQ*abC-wBQ8q}slIIRowAtQ2En#Fcc!%f6lL5|xx``ReIf$>YRF?0ga4!#??u z+3XdKr>IA80*Q@g(;**VNr^ z?pJ@GzW1~huUdS-Q+hT0?zWEGu;JyH8SRIOe<5Eer!JL$13BL*JN{G3o?!2zf_?ru z^mP*aPq^k`-XsUx_Ceh}W6KR{KgSqlt%ctW!B@aR<|v^JMDn|E!z25~qvM023Elp& zPZ+J223Iw>*WXVa!V~kl9vU=f^}HwDXycfb=hmKd*Mc5eblq&f$NE-DHns4NdWCbl zkax_7S3jlxI=)q}60FX7w{Un*?=vQj4~6RSo(CVNY%X;z1wNbQn@YMzJ|Fmri7q+Y zV$o%^y+Y-)(&?+{Rq3Uq?ZqDEnSg)Us2s~@yNEfr@ckF({jqIVx%xb*$d-HJIp z(~*-roCjx)*bje@Sk{GJZzjI0o)w#+$ujCKnCPD}+swJ5V4^Xlo^yz5%S)PdUWGJ= zM>zTia&nKX?@I(j{FAYvc+qRvW4}s%>_PnpJU-{+z6`wgRT6SgXJmf$SPc)K=v64j^gI!`H|cIk`qvxv9{;PaBs+LM87 zl8KEyxmyRRmu|-xn2*l-D&&%_uHbL)@2Xu7uc~HU^U;=vGbqOv8J+spQsoNX@BlKw zBJn-u<5TRkDpU85uAy}*!@KOVmAreJcP?yw3S0eGuy3&Eu7NbNDVziGRfTj;;T9NLn4?f5+6iXP@3o42{6 z>~`K~@C104x{ROSK7e~8Q;eUN3?UvNb!bl~UOrHtR?(;CTh0wmn_Oe=mM$sTA;g?W zj`cAo@>$(&$qu(Lf1ks*;%4mh11!1r-;cAm5OW8-c(`b<`ak>~@}&~qVE-9W?Xu5G zwNQ_68~-*7w;VjoHF7L@`PlrIExsd~mM*AYJ>NR+PSg0j%(Y|3NB*?zgR$#Ze4gfk z;LJGI2j)RC5t88};Zf;^XH198qNJ9+uFVCDQ*{WECC z`QLkvJlGPTH$BJ>nxkEoTo8z!C+qQFMP7c+6P1^Xv5#2)6UfWXJgQw}zULg7Z#?}j ziP}V>GT-a6<4Be>b*?XJ6GdjGQ%?(gq^~??gmWG?LfkLZ`7*qq`1HdE z^IgUc={+}ppgrdF@BsC}WrQ+}^qTvSPwjZf>EU|P?~_g2T3348j7)6b4*Z9ZkNw2X zVrascczu_FJxb#|o9yG*SKfZQt5&kk($3FS6cp`nZi}@nCO_ z2aXe2-&=r($}k(pRO$hRwjPm*4!k&%-)mspV(s^_LcMBDmz^(Ib1g0QeYLL zzO+FetG>0=R}W3P*rQ%by6miZ%ya1#e(h6EDk1HrA7axxGxK1y&34a(wHxlEoYo`n zgE?204Z**O_C2)Ed5R{%Dc+vMxGg@J*w+{x!V`#|`?T*!%rN|&ne=HF?F&!qT+`nu zK}OMj+sU!^c`n)0OX-KB|DU*}2piwOTYPgbQ6ELW%p3ew}*IY zv_CBTTu=M5|1M*G0(?7{{ozHXqS-%gp=q%%Q^ zmnT2#TS&wHKB5zzaU1WqxhDrTc4%VwTdwpdZO(%>Gs-`NHd|dcK%4jVq0LV?*RUux z(DX30RO}XQu8g70j)b(i$(>=*rt0(#9vP+0YdXG{GNfrrlr}j}w?O?6ZRXQot!G}N zw9#jF6!v$9$+-{feYbax;xd`vUwhlx@;>+F)0v`I>;cai&4t0AAH^Pk52?Xf+wHuo zT@P^!a)udGu>IL^-CY02;*&&}HO?toZdHQ2!hefOK(ZC3! z%P=@+pF2Ldr6cn348sWiD|tmnTlhto%Je);To0Y!tQ;H6zqPc{iyw2!u)2j_pLcZX zRL`PRU+U;VUl_K?;~O+OWsGM8e&UOK#Plj(HEdBiF}+e&WnEcJ>_mgOgTgIOeUWe0 zU}HEkP5B^j*-tzf_GOx8!`q9>W_B5mut)7TPJ4%OX9jtixnI>OHygg>lvRxA)It8v z<;f|;f$%>#H`)JS>g9%Sw%7k)iif?(1m&Hwk5Lvo_3%{ku@D!keb$8G?_#pQMHP6!a5bAuDI`4e&&Iblj$6Tx4L1E#s-W%xWVA7ZW ze)$8cf6!U#ci_X?*9ShvFyrTEgU{S3eC`A`_{y^fownAe{3u_EE0858F6&c3Ylx83lx zHfXGsb!$53$Cm>uXeR9w_?6D}0-v#4)=LH2A4>XSPP{ds0^x|!9Ul-SF(mL@p>(nk=w(O3VP{cSkl6UV|lXK>Z zi&MQ}9eH(@E0umtcN<4qNz38)17f_Cb6%kMc{9YB1JY|Y%-U>(N@n;^`_D7FN?n{K zE;5?VCV%`bbNemJ=H55fL~l9T#*s(5^Y+NiiSj-4srjNd#o^ljXTNVV{zT|Wt||Ve zKNm-={UC3Vze%{DJ44q3`&r0*DbxH-OQQ2Umw8S_-rJOCtXKnn3Fe$fLZkF@N70KV;+OUL$xJJY_%UZ7R%R?PDj*KlSH}xjV`= zw0+rx1*i5<_p{vF_01Klp1h1aAGrsIKZbwh{=$Fb?v}TvzD#^RX;%pAP$*R<8JYUsvoW@=42?Tv?5+|`ovz^_`CJaCt3Jou{%{SVxg zY&=*#iu{T#O+B-cQ&J1*E4~BI1$=v^>9=zHo@sOMdicq)j~Mgs;{VnOd!}9RsQv!^ ziF>A9Px_6lAwRrs_q3ZxAI>?N!CKD-d*-Eo-I)I&=M?`;-(SqCnpc<=nD<8ZE%Sc& z^=0!8eElEibw2XX^Nv39)p>>IKR@r+m1gkG>o(2Zf1NQm?~!BkZs++vkHPcaBlphx zvwLWmz4IfxIp@BKy>s!hUopm-@q;%iW>LlNoCm%M-+!FF`7blhyZ*^|-~;!N@Dg)K zdmdx+C3{nSql^XKu7N)2^<&@CD}$F%zQDZ1j=zy?$LX+TzaCukATz?Jwi`*Zw;9{T z7ahK4fD0C%Bu}aGfKRdCRn|&JS5KHeymxwJrVH83&HiZ;GG{XT2m`PQ48*2`Z>!rC z?BE`ZPq6nYCcb?3Phs!vME;fzWSy-TnB;qha{(3Xm+7opD`yWjIqxqSQ?%!${gaj| zBls+5_Ksn*+>Xty1>cO0`9=ue`={U;N4_v@(}w(t)la>}dp3Ta4^&h>c^pyS@tis(^3le`Ki9 zxr?;xNjr+I)v0s2E4yZvIR-xJs;Pj7;yZv$W~3o!E_j7G8@Vd9QT4~?gJ&+TFkS7k z6;C(Ew2wpID|3%3@VPq*#oL#d+19rQ;ql_Fr;oVT|BQJHa8Il412!gnlC+;FjeAsW z8*g^_FQm27zcyrEYdr3!tnuvP98!R@vd99Qx8+QkHP&kDTQh#Foyb@{={5`k`0cgK z_lHql&qYFhx2~ z-t33!;Y%s_78$$eobu<*I#tY?*yag5IfHj&!X``pNcoxFPp|sfSQNT6+RqQ&k8Kz< z(G2Mv=v!95Yc|ZIAF0EQ&LLf{P(-#-`n`^M9x}@4{4@QRjP_?@kx!@p&w!t=k>@Uc z#V21|^`nhL(v8kv)2ENk!8Jo%@Miim9NW}e;NPy`+*8Kb8&4f(eE4n^gp7{Dh?&}c zJO76ygTufyhj!#sXjB>v*p#LzEwNr|?{N)+H>~@9QK)80py{#AH~1c7KJI6HOBl}y zckPC|7~6E-kGsFQA&0(JTJta{{1~+Nq5Lu!$DQKDhJ``wQ}2O!{78zrIB5qIH}DUG6m|;sY~LxL}a}%2>~$S1cL@~ zuA{lcf?=KcG+rnh#U;#<`jJULT3!AJm$*IUKH@W-OfiBh2ih{0!>7xEjl-t{pG0>2 z7d(FFNA9$}$UvUTftLL^H_6{H*0XDxE3js&8K|6U1Oij-?{4e6#U}z)q(8#*D9_iY z=1jHuNNhbN)U)g?^%Ngl{bbH*cgUwnGxEX*%~~HuM2Ib&P~xeORPFMV?~|!P-iK1ddepL zEOx!neRt|-e{*si*<1S%(%aKCC;xa|XtMkc+|HcHpCT(fkv8@+C%>beS7>Kq*_6hY zIOCs%&$IGsjN(_yTR>jr`!;*Z)xg(-zOFfvKa1ALM6jI0yd+BhTVR~>DKP$fVeyv% z=MIba$otpA!t*}}i(k_I+3?L^&X)}^(*9aJe*RPH{d-~j-M<3HkN*uY{?VWRMtqym zm!`iQ1B((De!gWdPK~GQzZV`iCxpkt3F&Y4-vE#Dv%sTm+)p{%)Ym`U*_RXF$Tv8f z_J!+0C*V(Sb@OxE7waPWQws0651vp4@5nDJ{8>Hsz8+`4Nc_>}{mPpj9!=i0@Qi!O zn=ctJ+4y-pUuYZm3yY`8F5yOAUc=tEWa7UU#;5VW@5|#>C4})2^8U4SJ>o1dJ}dpX z%s%VLe%g2FTJcg!1|VY%DQa*HHX985Fyrx0bbd+Fr_gx<+V=a#>t zPWeB`XS3)g`DJzul9B4RJ6wp&{6YhGHWchOKBgHbl-}S0xdX#Xge--|6(s(m1$>8r@s)L_a-p z0=waxw`DGpZ^zTa@gWIl&mr^5oI$b$)!O=hjGn*j@uHA?0yHk}vBpj&xdpv(ZBeLH zu!`#AY9kGM*)!<5R(iA!aMC*K@S8Madk219_x?fkrG?)Z%w44P$v&s)ZfJHtI`xm( zpI277gP&ZCt%X=MMiu%~75Y=v*Qd5pHVc~#@pL=COP$TgQ}RdsZ4!N&-oUnIM{PI_^~47HMuy=$TRdh<(N zwTCEY%WT=<*XU;nv0vpUEB|oOn0$}-c>LmcWGgg?i|xUO%VXN#D?59iE8mN* zYV{M?Gy<2l+x?w_rC{cx-3;2*xpKi_18|7_{LH;mcNV96Z0L4u3`Jjl$f*MQmCloH%2Zz*Vw`_@~-AhJCh~va)|a@e@1t zm25V$)mpY6z8!DG-a`NGVhoAk(Vy>=1LNKJBbR0PJ73{k^HO5M>I|1)kW2fbMGxtm z_v`d_EwSvaY38O4xIfP+{nB-z<1u=utuv;FcXO6PGM044jIzQ;c-nT=V>)(Aix*2@ zi~5E0EfN0w@zSo^;THb%){h;Zrc$lZ3EJNr(|!(muI{CaM!3JSPsWr@ zvvlahZKr_;v2}RxiPK%2+LP9Lp?fx}?&S`5{`;8E=?46G3@|*;5Uuq)?Kp*=}oXv(_DZZEL1a!gtx80=yRiC;hj0Ma)_jJLY=! zvTgtmEgAto9J>EuL;7JHzJ}`bOBXd^~BJvdivc9~gVF}m3_JKaeTxY*bm4thm>yy z_Rsn*+4$>ZKWDIqw&(GGFXvND?&Q%O92$cY6EW6)p>u0~V6C?Ads#dtJ^U!V7(Z)! z+&+!TmPbU3E#xb`n^=_ilSuYpPi}oIJk~l)92Xw zitIE!HhAYOU4OD&?Ahd71{uK)$wb&yCgD>d-KZ7Y)r-(xJ+xLe-4X(WAyL990oVj=o+{rPAS$Oj?X~U_f!W?eljRF6D3mK?caZk8k zVlVHGzT^1UAm<#2PxI1;HlJ@}E`G8qn_Fp@1@~fdc7W=OmCt=s;Cd!5(|^A#D-zhAg1o4Bq8f@7|^=@0Ld=qw>=QZxEJil=#u-Hj#W)AEJ@u9F{`&Y1MS*N)~CSUTX*|-E-?QVPt zN(Znnh#a{O`B3|2>OVeZX|MPuZwz3^5Pa&XFBU#0iPw+sZ+k92A+4*kHr6jMZ7d~L zQ|srlHl|r{1vqN)6Xvr9ns;~!^SR#nZ2(~ zwxS;Tcl>DOgW9pgjm(sU?39ddG63BKUCxpXh~W@D$0nYh367*O*IBs%@HQ>{?H4#( zQjxpSS+5QAmQ$Q_UtBsCoD%);4)1B2&igjz=l*5a`LZhfPkDA1)}&be&BT&;e%%Hy zvD+1c!N>di=!+>8B`@T!DtY1kamYIGc+T|utazkVwrsO$y1mbrOKkKAepM%ajK7YX zdre-z&%*7QQ%0>s2KH z(arz!80X*L@P}T+1|Yub!(U=8J`i5^`__S5!t;}yRq-Sj*XSIbr}F6y>;pcp80x#o zE8mDs^zXn%;-d48uc_rDBOld2kf!hOrOx}2@74?`3T=UApXO|U)(g{2kJ=S_rv##zcZ?`NDP)SXc}v61mM zwel@qk9X{FPml0Vc}X%gG_APr>f*Ugk zXEyZ3vjH*vcjoc6X#XAfWqOUK_1ro4VibOf+V5}v0<1?)`+rK%{=1|(c<#*KvvKX? z+{pUhTbFQfTy*QesJK0{;|stjHm&u~ksS}lr4|1rvSWQ*8uz>I*cq4RVgLOPq@B$t zSiTwPCr6ID*K8n0!V{k0z+f7&B%U%lUKFq1#aOh?+@E0tiMW`m0xEL_~oT>61!9Xw$te4+-p)$>j)oo?Rv6lwZi2hUgu zE^dKme4h4y0v^^=ce*9hW?DAKz3K<&oMj7q;d%pmpw1l2r($Gy9%XhezjE@~{BlQk z<8ALgnw)PC{GGlPM|~J7jgFV{@h32lMPy@i&SyP9TRIQtiTM`47cW2ZdTbVDj6vra zj{zU8RhAwJJ=;9p1Xh*ZbPM0?#yfukyep2uyX{fBb^HN_5FpceS6^fCDgR7lhhjA9 zjK1lSZsH9$`MNT5#uzJRpg(vXUiH*0oxj|CP0&|X+M%-vvn!_ti+Lv=cUbl1az+EY z1wP-zFJLbg->a+g@ta3?T%3|-ELf6ftQgC?@|0%<7B0W=)$I2N7=@ez39lj6yFXfw zi?bv6W2|7GaIw|q(rH28?@Jn|o38R@$d%3R9IL%HxAEhgd}Bp5brvvhv-5Uk@4Gy( zaEY0^d?{(Ed+c`u-9WJha!FH}f>k9?`d0a$blRJhWGs-})5>@J(N=5$#-3Ht@fLly zXT0WgYrM#{;rKcd)teg~PvCT9qG02pkFv`KxSMdd2R!ESo-R44zDXae7^eH))@J*5 z2eJi|cI1NTmm4cei95jk;sx>-ZrE{U@Nr_x|A_lMU-J&F*=6Ruu{PK6F0Jku0S0iP|3tZKTkz)b4&&XV0Nug4K)kt$;H$myC+``T2G9 z*){S<{iLT-&miilV0<+@s~cH5P~8sIaRy90DLXbJGK z(l<{G&O%QJ4e6LPL45)X;-ycspOAm2Vbl zx_8R1-%bmqc1%+27dzi%E1#FNG$(DdomQ68F-dV7?0i$Ke6M&rCZ#xOo9#3oX^Qb^ z=eyeOpQmF|D)5IkN6hv5oBk6T9RI+bjnASlzx74#Xc%EEzHu-*5%BnE)m6bKfX9)t zFHatpGQ7rc<-Bp@aATsII`P+Ei0}T0dDvQBqmIu!@a4vjsUzo0#)|x5#^U+u{-y=g zG2u^F1)+msW0!w*vf*(7-kdi+Gt{nQD0K{>j+=-B_&Rk=c;Ksz6F6r!ojN?oEwj(_ zH{lcgx-oTP5ZmZ5SLuq$dDI~rS;}C$4(4LvFzWD*H+=ZK`i4w%`G)!27H-hbkFg~;%k9PxyQhr1=}n%H;y#c)wFibo>jneD$%>j>FZX?@1*=p$`|ro{;7w_ zdlm0@4Y14ar2I_MK114I&cE$cdDhOmDF1Jik5K-0%3sX)2<6?pU%>lUlI-#k%HK}; z9MV2R`H0FRH-Cll!hP|CD#{>eq%Db#ML4!3Iu_kMmBPIm%gLVzu3^TsggRTqd-}-( zKYu;9YtJ-;_Us9UOH(-q0r!!dX&}BY6 zGlzC+cwfdEGn1zxy3QV^{&9D1A=9z(}ADKhKA9a-+e2%qlKhN$}4I8)c zf5K|l#y$Lg_2FR~mp$y-I4&@}Cg(ik?vi0erRJaxEmQ}%+uf|}`TZrb)S za>+24JEdXOkc|=atMS9Wxxq7{I{PlpSMI<^U1jS=%stggnc2ueA0Tu2I2*GYnMr)m zppU|N`ADme!cTZjuYNy9pKqkkkDG(TuhF+}aqsdI^!a(7&8wc-_?Xr2K^q(ReQwpT zjn8nG_4sJN>xLJd+FYrA-+1an`tVrgf|^a0H*NcXesiWL~ zboaMC+TRxX`!#Gh$P-s--&SW0OJB>aDWlKq^Ud+CT6HkbvN?Ie^WU-0m5HvEm-Ul9 zn!uB#UpG1;&=T*K4YO{` zaKOuZifMFulTDwYwKO>}Z4NLHot1`)9=f33|Ih`a{12(VoK;3h@=~twn|z`p(abJv zZ__6jD+=h-th`sUQ=kd%xk8Qh`xBn@SjKj1rrbS~&^hzsrIbjDiAD?iWYnV|e{t%~u*-cA2kp56m~F zZlaBc4%@J5c2kbCTpOZeZ?0NLp0&UzEBtNJj#1ynD4eWzILq~|Xx;#D3IM0(s>h}_ ze`7`iJ`L10CVcE-`RS*H_mQu4CU!~g1=L*V%-DY@Z9ZuiDb2nsWVh1T?=QVVY0_Co zhkvHDJ4pL1Y2UcMA$OqB(0Y`6zs6}VVsyBI@9N_!>o+^R()vx)S(mg5&gYk3pu1+g z;nUtH^N8~lxP)yDE2jhulpm z&L4Cz9?k8_Baw;sQMYWVs!wz3VNNwq$WI^7-tP&bPkdZnloAx3oMvuSu7i1~7cH(@ z{nY2UYvFz#$%=|^Alv0+c;b4O?9^kz?ewR5b?(L-&iR(94By3L_3dN6iN|{Kc5x0j z|NU0zxf7f7PGW@ND>^`TZ{R@trkB(2@@l z;EUiqg7!B=>C3^H!KB%BJ2*2c>>({4U)-b#_m$TuS__|z!x>;DU*_pnzTWt9wbC4X z!SA94m`+rhgD=*ZkkR4Gl@^09b>O1#rGBi9FGrsdzKjYVv3|3|A6dUy;Sc#$oWwqB z6f(c<%XS<5>Q?yHKfu3k!B-<1Z)wi}Yt6zB=s+oZ1uKD#m%E>Z7o+e4s|U|Kte34n zbcc$^@jKe+EHriQt;Yi_WnML@919(W&e@i4Qo@Mwmi@z z_nXQV^STdu^OY@cv~+lA?jGKkZ?V_9z>~8dI!<4*Zt!=?0kEk1;tpuSsUy!ZS?y}qA{ES61Cb;hQLNY|Zn z;w>wG9hvygy#LoB_xfv)Zz5Ilxf=vN4F<16l#b)9(VtckBX z^0MNyM&+BHsvO!N5GRF@_X|`ky_Y{Kvxuhjp53@;+Uk}qs z6U~;ytcN=zQ$lUT*pW=t3XP;N=6oJ&9)g?;2WRdAXB=C~l99HKV|{-PRB<6VgpJD5FMnY~^~>^Z`;N-bKt2K|Uw@49(%%GU$y@sNGUrE#H<77# z$ug7pu0AP`(q>}kc}L%gUD=B60FQg!V~z-V=m)YzV*&Y$}@1^qmL(gQK!FDc>vnrc$N?c~H2nIuDU%>DKgnHf0r`b}wi2 z<*TGV>VGl+)mO=wd!uz#^K12!a#lZQW;b#tB>U*Z;N7YimbzOjEgaIhvVneU-6fDH z-?J=x?}yu!b2l%nG{%~KbE>o7?i>~)AzwG*MnOJ(RrtnlUA5}B_ox%nAxo&y;Ozlt6J7q^y-O7I} zkIG!l6QBQS^8XRLPAU5YWfu{Tq5P6uBs(Jp-hxtB;K4sDeXUyu`cN6q!1$j5(Gw~As zCHVdpw2kAQt|B8S{dqe4!B@35J0%&uHNRlvBGS>V_NC)r5{Vf23gh=jKKbm#$C)e zwvC12ZBALO>En6k;j@E}%>JS1^_NSRyaF2`{NOy=mfcik#0QmMZC%QfsLWW_YRiUt zXmD`CAHLclSs`k7?JleSR&dt+1Nga9 z&RX*}4`a=<%K@7M`>?^ZU$}2W1?Ac)S7v$(7MRu9$orFWU4}IWzA>yV*dW`GwO*O! zt7*INl?~I$-$wok$oWn_%_nicEE}S=UKEW88tfHm9`&uS-i!}JAAbPul|ok)#1C@PHY;t7NxtOnG_3)pV_oDM*qv4@yd8)i zH+o9xLZdUlxtWqR>!vPUQ!{lfvRc`zMw5JCh11%j{Db0IkRRT@9b6dU#IMktj2YWi zyO1Z|XUvjM)+AGJ5|6u~#O2~Kc>>cEH#aZ@KYxCW!JMfECq!c_5{&6i(lnk}TD&8{ zn3h@jQbHBCS^D+svZt0Xp7?(CJ*U%i9@m3!lH@n1KQ~fmJuu!G1LI<)0pp!9FrKM2 zV7xO1#-Amv)P>Ip_5*G(vtYcL{?q|y!8p_l#_8co!5IGN^w&8H+qb_qt{|UWSd=kMThEPO9G2YFwyc)Ync`y>Y@h5Qda_BX=x zCke*3gS2>a4RiAAq=*9^|o z=$CmVvb2S3Y5bOkk$V_h4LU>Vv-bNXdjB!+=n{K*m#i>~M{7yjP@~C`D;AiR9_q*y zk|~rY!gt9A9{Q$o`agwV$<|IiO1t-Nf9UBi`$K^lMzD5lpy^5a(u$46@deDpcmF&1 z{zY74+M8XUKNVn2%E5*rd#`Pa(wVKy@R_&V>vy=u)V$#uU8C5niVvmpKOS`dWfk(_ zlieyKe2jb#r5c6z{&_~|>Gx-ZBuCcH4m919s9j{vh-)^W18H~pb)l#K`MOX5T&}%4(Db=P?c#IJStZ)NwcmDMw%X06-BEqo9UIebBl++> zY}$*gb?>1YLQnnahEM>Sd3r;jsgSn!#=trQSg-$A`8s8_?{eK@wY^DatGZz=c`zfq z)oMGd=4-^=U(a_3*2vFK9a`UH(V^~}G`8_k9y{@d@49Xc&MRe7KtD;+!ZWG7FI&dtJ=7U4XN z@}L7Yq&~;}F3?OqeqfTDBr{3gK}UY>u}96|H<8UAFDswi;2ly^2fgUKWGl%I{aTaz<| zZP=Bh+j^eB&Ru9UdHkH4!AI2_t=s8CeBHCV22MJ()d(JTOHUmcKGvzbQobbJ@wr&t zaWgW2vuEV&7p?u1ttUp@N!%NoiY_?_opLa`q zzFwWAZ-gH^#%o#80Se z<$T6GkFnnf4CbOw&mkVXtxMbZ_SeZrKKdKKK3+gt{2G#@d~xfe)_{uD-D}8mj(n7; z?|yXPEavbf;WzfMcpI4JPWRr#jyc4A1Un;BEm|FhL7 zlkIQXRyC%v27PjyJG&zhO~mIBO{|;YZwl`j)3^>DGmK46G_iI$GMD?j#zwRm&;@r@SiZrBwn=xh@kVx_ zJHVZB!0>OG>u2FN@pBy_Eq<>5P5I*HdXtr}?_3|F?@z74h5}zp2XAE;8L3}*;cyF| zM}~)5`258__`KVq^B$h=*n}ScZ=2Tp%BlU!)%VlCdfJSYtM67CYvaxsx%w`pF}7H_ zx|FoPMy}q54wXo**13hV#m<(Uw-ejSDR5W&pIK4eAeOIa9XZZ??|j_Vp*_wD?0T{M zXd2&7^L^9Mk=R`^sdF%r%KTlF3v?a@y5AlF6JplosIZi1f3{ z8Me)G_Yc_jLYF8VVRU9dSAjJzTlZJVo}+!kCVjt%@7OJI5-kj5?3xxJu6!l)D1GsH zeZQLTS94Do^cGn2%c&*$&YU*07aP)d>^Y_L4CHggD(Za>;?x~8us?Fg#2(uZ!IByF z=sr2_gsU=wz4=d0M@m+pX-OF|Dv=K?cuW`+EO>Bv@{!({j~jU)z7{E#a#$rz)np) z9`5$?RC1TvttI%Sa;6BsL*r3@gX^0`4PzfO8*(49{f@*Za)$+`<@(o5&GlFERPh9+ zdOrT)!u(>RDgSbwEFQyXk}aWk{a5~v*B?*Q&i%}y2gx-akI_^Q-g%0!hhsbQB$*w# z%xQg!jWfrvXWdgEn{-cnU`PL3@!$6O*8aXH@=tQ^ia5bs{1zOwKyUA3vIW ze%}FhZv(sHLH>|@yL~sx?(1Pr?Ed6x{B9FE=N?vQD#ZSBd45sZmoN91-DM8hH=pqn z+cQjmkF+xO!Dp5ntc#9)XH`ze=5e|M)EjPQ>~+wq()x~_7^`lZuMEBFT3>{*07q-= z)Lrm8V}6$BF&^TBT4UddkB`RQ%3Q27{7vh??{%jK7OvwAq-?eNO<|AW{Z9rK&SqRo z45Ksb&god5<`32IUvWYoTj1|}?}tWc>r7++yKbWiJKw^$iD~*SaX{Y#1_5xj&b_3= z_=NZmF}Ai!WU}x4QF>KRzyJRNqp^PljONF~s0zQ5B?FMb2O^6TdwHdoeOwo@Q{2#T zPu#ay+EuKQy<@S1@>u&wtPgv!MU?UAzsdh)@Sod9_(OZeXDk}G@1IVluiQDm@I7d} zYvnB3ET-)nkpHiT)?CB}$$3L@5^BGEy4EVY6x`>(z2~R>Lpu&?9cO=$1%m1D)d+W- z>W)N*&s_|URUPQY!Nd2QuGJd83tU78CC!dkP~Lhb8tXJYyy4%vYGof+K9w8HT_0*M zQU7+im%Q;F`28+4`i^K6xo76ED4jA+(P=JYFwj?(mskPzFZsUl{jN+S`Wwr?-UiNx z;gO<2Z*I@MRL$@t$f zSe^wh2)_(Z+5=B&?dD0Dak575bGim}oM<0EzTRr;bKfO7#@-Kd_O?stt9)H^khxz* zKb!@=dz0(#&gXoO5#uwjb*gWS4-YaVaw5LX&OU~C$9?ds0BM_PbDM{KewQ(6J$$2j zY|s6M_qclB>Ex{A+G~kDqwb^+96)>@>Z?_KT>}fNu`A^K6F!{i0zUeTUu||MvT0%4 zLv?q<-`q?RpA`C%%6c@2HE=L%pqn+&W!?Fum}hFgb&nAoX{|T1tFbpno8rwi%t`C*>>ub` zoNet9zU>7*lC@hXuXRQJmpm*Vlux#F)&8EfX>UwlpQf+aM$&!_{JH7=bMR@jlWI!P2xLSZCWvq?Dj+tty|T8cDQXcakQ8>?TOfO za2DU}z4~n6`^RXT_mWl`(8EXsWRbM+}z{bK9X+hsuXp zcAo6;aMg=1PU|InS z|1qa%4_Qa_CK-h>9B}BaRCHJA(49>S4&9YdUbxxxD?1)mEB_PG{!^s)rIQ{$=+Gv* zJ9`l(v|-y8q%Vplghv`%Y(L`r@SSKM>a9N5G)R3GjQY0S`@3^C_d;yX=+aMkmJYV! ziswA1yKHw`F#>z@Xd~?8Yom|(zi@TrcfI8))m?lA{-%$(2if`_)yXxN(#uB)A9;6p zxA=qjk)DV<89p(D`5nqU4`aTEGw;M0Tsac`OFoYSS$77&E0VE?C2?Lh=INM$4Y1<5*ZL`;k)>Qm$BBt6I4!Vb61?My@i-t~`ij|6cq7jxJZirk{=A9y zxAA-X82^)M=YmK5*mUs!jE1I6nyVmF&cY1k%IUWO!3x4sjHk>wZb0hPRw+Q@b zVLYwL$i>`OsJh0%r>Ntq#By=sF|<-gBnl(J%lja@yft68JS+Gd22ReL`Qzl;-PoTx z-q4&Su0Iy{%78_M!Py7qLwM?o;s)#W0jK^%IMHm?-(!=EU-vA$U17_|@~_(r9$`!G zXc7Jn<|4>*sklEI%3X zfZq3{>Haj~+~+tWdLQ$9zoqkKggXSM7~dz!E9#GY#I_(WB5z$K-}DURSm{%Hd)<@F z`P7@ZOPlWocz2R=haUHLv{3(H@ZzO^6^`0_W3RYM8{3jP3cr(VOu9n2FcZH!$;RBx zq%{(Hv2^S0%%Suk&8x4)&%E1FWqe{I2p)vitEt3 zd}jF0xbhl@c(CdaAFh-CJ!5Lcmfh^O=Wm+k5*&?y*M4Ye-`&7g?cdwOzaXa@c z94PGt_mSZ*GS=!S4tU^Io>BIiq;le!Wx!qc%EjY(ZmhdUIb5*PoMT1-ez9kVXmHGZl(81 z54T`PeJ<{Xx8?5M^rd?W9U6;YQ!HB+@bf|=K6qtHKKv0Gb~ij&en9=fbs+GlkAbTf zTa9cTk{KMh&O#oJUwgDx#izx?I`;eS`KL4c_SyDPI@`RzHNSWNJn5`Gq6zvJJNLwI ziOzj*dt7{3i-u z?W6RzNyNg_v125*>A1nizI{7{AG+FZrf*x3$F2UI_xJR#uf4|ZW8d`t))HGTw{1F` z@eLc<+?%#JgtfQ>dN96qZAUZq#Vm_ojtnnjj+}Jlo9!>{`9fn6K3@@+QQm?a{}TMl zfZpI@Rvc={3SBPzNue_j>DBNWZ(h%y7d*Z5tJJ+0JTK*2_u8Inbnc+8n`uM1Uhm3p zJdXU_d^@~yCO&5ejs4QEe#Ck!o>U)|UzXBu>085p<6eK9ee#=$kDp2X()HrY)=^e@ zjz?{U-E}aII@X{M=bf&tV;$R>PzFj5cO0~QiaYCxEoux6EIh@3(X;MeFd5^anf|7^ z;AN~lAejb#Z%ZDS#JDnIY11Ik7s{J-F$#JK(8}NtvvCNA(sZlUZXI&^sfWNyL zv&DD4Hs2M$5OVp+L5sz79JmmJqA44#rW#uow5yXsa)zrG+aypnLMRj5C6-ym6*qJJLT>zt+$GWUNPl zm9xIua!q=;b~bp zWTNu?hCCI*8_E^~--LG(RQ2^yPcwAB#^_uO4jJf6lcPA{L03vk0@q`7CE+GA$^m2? z`@UvibJ)$gO4`q`2^@n@_J@n!Pr=1B+HHlN(HSgy%;3LZr1`Sw@fvg!cro<&x+9+} zzwUYPWF?l*M?jC-Uu))^GPJZqFy!q2fta*D^!1km^n*4#s)*}6zI(e7dvcc_q%|nfeS*&P~AUE8}>*gBvGqnTf87&t;YE!xd|PN{RL}6zyl* zCD7hD`sRj@S^oTq`JP|>IA@uu$4(`?MyxG^^SBn@cKYGKtA5+GQ2p)3{>@d!{_k(K zebkO88|&+;zSxlnE@S9Vo%>52=K>d<&DdFWeur$JqD77OQPyJVR*oFt@SRlnPBZIk zqBbM@;UUm(fAsP8+_TfiN0b)~6WN+23&=()*`b|$Ew>xNHd|i6KQ_a%A0T5n>$t^x zz*Ebv)Zf~Ekb0c8y<{W2hc^1-Jv-=o$ys>MF6yd_!ZBdU5%5-Y7|HitD{UKzV;9jL z%-Q|NZ96qKo8axhyA^oH>pj1+WH*0jRub)OoiU})(aP%^`(Uqkl11)Iwt4GC}NqO~;a%*p_m#Vi0XloFc;!>-W zG9k7N(pu(|g9Df1@@2$$L_In)g*|4+onK3H<%b((C=PoF*bnvRd|N=6qijnDuyIeI zO-s(mpBS=yOH!22ZfvCJvt6}cMF!tG%(gjUtK58NumF9e>?c`~nU=m}Y~+kUYZ*=3w&@EB`>O{QJZvI54^!7>Oq((z7ShADK-yzc@*C z6-YNXP&YJ{y@TaL&--<~eeAC{c4(tPeewM>D?wq-h?9pH5 z@I{=ykXP9{UvCloLVc2K|MGQq|Net|967+@|F#@&gf30+ZZMsITeqBFL7ufKWku(c z%dy|t9tVxk;rp?7p8{WEEa8=1wa1pQ*W&r-9vEo*R+(1bjX)1qhwqN)PgQ!LNc)Y) zz>OUC_rj+p#?=9zeT-NM&P!?q$!oP49cg^v39_Pyb2Vt^DfyKk~hvWoRDh>whx7LHDL! zFZ#$-WGx>uVj41HIyRdOY&P0IY>&Q1NJ~RIv%SUVtxvQ2b7mRp{&Vy9@^5wE)MfzetUuN!l)+eEh zpU|s|-^a59e5Z9T|BmyiliWSr%RVqJa<1-2oNdXH@;OPO9r-dGdy%~^_W|IjSdWkM zODy^*eK%J9@*1m=xt3`!sJ)cnF_t-Kwc4`fjvwk9*0-s_zN#?ErXX=?Ir=3}Yhg$NFfPXp7pLLc8n{(G|*v#V)sn za+;eSbPAF=Dxjn25tpub3Vr8f^5;pPVzyhnxE0)#4njWIYD2P-_K42<`y%zv)|zGP zs^cQ+P+o6m+&A!Ed&%y#OTeY`!KuaI)*|GB^RPMDer2Ky@%s_snxhX<4%jH;R-CZf zd6BWdnhyTpm3h{8+LhpNh>*)e0Nl|;JC$$LWUjR)D}hI^K3V%f-Z}cXLfSYQ^Vxbr z<>PgPq6d^;{`r`Wkn3S;57@Cz2Izcl{T;Gdxe@YG=3Z>D>a+SU+|b#h>qO6Zb_Bhe zeuY18AQNeQ2~YHFwZ_PNzFVtx*5B9E@i9&0>*n|XiT4=6y}dfhY+!40jw{OZzTQ#Z zFYCGUp6Bj+*_nLH&5$);F&_<$HPKmWPTb>Bt@q3A^?sQ3Uh|p3*E+O)S~##^4|f*l zhW?p#)mty`-;{mVu)xbq;w_2vUCL1)+28Vq3D6FE@&n+v=n#G0 z;)gdk(U&N;51j>V!DwW6?eEz`iXS}joOg@9ck*f}$Hsw`TDRI4DDD#(LV6eU6VbT! zv>bkM{s{cL`2V8D&YDY9ri}Nu@$cqIZXKB5JlMOzeAvWSmCMu_`vI@rXYv+op%1e= z81qi`7ytKmWCLW5!;|3$;^#IWPwWpSnA`+R#It+oR|jiPvX$tVY|fIMz9V>uzF0o4 zE}IQ8mMM(0l>fgnr-^8Q+uv0x7Hz?=9N!%62{*AG!knuWAE&Aq zRq@6u*`7`=EAp)(2j1t5nUjmk?`UJ3HqXnBJWYKSvNzCo@#?$4&3k~0;t~SPpY&F` zevfr88t2&0PhW1+I5)04c&R4xO_y5f_sF|OAc*Bj8h zlhIjgUWURmqpXD?m7^cPZvYQ&26h^s6VoTyAuof+{r8q`pCsZaS;GTkIu#%FK5%jT z(zTZ171J*Ha>b)j-f1n0KV6<=$E?%{uSZ$*`ml$&w%5-|Ub3 z#_~&|Oe^1g85yQY=XkRgUj@%BK30GoN$(kx@O^@5?}rAX9j9j!>%#vQ@oK)ygwI>H zBgyLAi`R)pejXdK{I3NI=U4B8*7`C-sq}NOxKY1i2?n9B4!%mB)4XK&qp{hMRNztp zjjdyhz^9kS%Ku3CSphA0JVtA;`fbea430|A+(&D(t+JvmUqe?q7_I&L@b22Fz(TV2 zVDNf_GLtAX7(KOdrB6c)XodWrMXM!GZe)B0-=}+mOCMq{RAz3RVA*4^>q!?byjVz?Uo%%)pH0H^IGcDf zFf=%eD~(?_UbXVQ==AG*Wv7&HP*x-hUcg?#g4K}LI(4kOSFr2j?+(pNw2x6-iJPw3 zK1ji(;Vt9`QGSW~Jj*uV^wY-~6F#0BWzF-AVb!HNSArZF`+c6Kck#>YJVu{iy*qi{ z`mLUlsB)Uj2+l`F-BCxLOZb88vz-R#Rj^NeO!B8&rgwC7AGUPY&hF{iQMq+t3G)Tr zmyRVn^5#XyYxQgqHstItiniO&uj0A%>6L18kUHfHb;j@PcM|9)d*{Gchn)g=RM1ufW3Hf$q2NTNWrIG` z(s!%xzB`SMBh0T2t3F@Qp|-v8Uu^rou30Vfo|iK{LEJJ+~o6XcID4F8#&oH z<{eS^THOqP(y-68W(DW}@>%vz(~a(ER@3}naNRnVyf8__ZZK==GmUTOXZj~K%rJJ= zXGZbAxR`lPjmZ?M$N!o?Ge5}~;h$7ty{|jy**Er6$7^MG&~-k)D=yalhBJ2>>N0(t zjiWPi$Zb3gdUMlmZ`RJ;y*F#8a5@b- z5p+%OO-DAL4DUIJUkSWX{88sgVf)$Nv%qLPMqH)FkVW6K z8AFuw%Exg%o@*;ci0@W1kCP@EJ3d7J)`ibP4Y{~hd9tTHGg|pkIb(!&H*zLS`r5pU zSEDCMCl@eyzvi%a(*#|=>76yrm;YOynK#{>e9wf-$@yDI*}EuP$TjqM%^~ky6Lfvp zJ9}DD{jMXRXAS-N5r2 z?HhR4R^Y|gw{Of3rW#wit_gIO;?J%#MZSw3>Tm%vALoAin&)g9W}S6CjI*x4@xWVO zU;eW^bNFFj^EV$jG(FI{m@-A2hrPfY#`)LZARqDY!$rKSFem4h@$cckfd6P}4zy!z zvEiMZo6mpG#iHk9iVvX2Jy`7tJzVbH_dRe&^7$JJ0*#OUPgm^+^r`0>qw`8=XJtWu z-AO&XsuG#f)&owo^nh*DB|Ydj=>O3x`SwbqlUUy!EzGf7hp2B`;PdvkcKmLX@@)L2 z1bK$De9YTfjB&5@>aj7N#Ta=OKSs6rAnob=#h$r*v(5F*T)yF1qHpfz8_i?qGaARl z$feBxQ0DLo$}LKOCyRRV1YJ9D6pb1Wz3N_jCA9SlXzZtv1|ecKq1 z`dRTByjHjf9O7}QDsG?i0c~jw+VvARPJ8UBmula5j5-|rj#5szEq~OW)y!eJ3%6HO z2hZZ?Q2G~#hFaebp+62i7ECfV|G>B@f!uf-uv`0L&tq#@3-P?#Kxg9MY9+bK1AJde z`H$l9iN3oc?z>Mv<0TX*rfZ`eOP9uZulmh149SCU8{OsI$aUR=JtNkqD8Afa&p5kf zmpRPXrMgtceXqHpo zR${@j0~T(%%T3ie?$ze&#WXEEx4_ug>0T zCAP@ip4vdF@_Ue{ccN{_TaMk6F*R~tlx5dz#qP=eys^C?_Fnhob8bH+_8vQ@`Y9OI zFeieKe9IhLg<$0Pg#9BhlK<1c1|wS^=)|!iYXP_Z&z$}W9@0Cg&+2b{IpN6Usrc3l zN2&+f1l>A7#U@*LGF&v;2&uj!-t1|&dcLwVw zn{}gWcU_yPD?WB5J~mo?FyI+2y=~euRtt4C4bo;4?<(kb4sGh%(JRTP#ld^!Zv4aE zE=wPq*iK>Z;~jnSfo~aMty%ewOK*BVFm1!msB>l2#=qQ0>Xzrv*ZzHDlFcAH@)gD; z+o5P}ku@e_O9mTLweq6cwEO=gU*wO<|KH0OITPCq>s{*$zhBlm^uhaom@jfi`Tt$M z$T|OfzQ|#}wsR~vdY%8jz({(rq2|=w`*a+Ppbhc4RSKc`E$q#u>nZfZf3YVm;Y`Lb zdyxfPAI2`=&|uloe9ejjI9&wtlPZV|}^2QwC4f3|;H=2vT?7krOzzDAj+COv#X z4Q2AGmcKhWY1|DJ{0BmRu-Zsco+i%{Jh< zZhGeEi2qgkd^NPPwLm;)Y-A#KizZ}X`@SzG;cM6%L|0$@R)0OUn@>odFvr_6s&!2a zqaVJn>!iiD44`YzCv6$Uy1vN1PP?-lpFi}tA^D!#evt2C@2pg;eDh>$zmV4@U9g?w z$tm;U3ChIF=kB^VQ@V2z{5!7%SsS@2c{A=Bam!@D_YDEUmM^=|S;4ky%n*2LF zd~?*+RS%`_j;wg>vW4gi%C2|h&7N}QvKoOdVI(?*QRo&%(>L;1t>v^EaxFQztd3!G zct0+em2@`B6Ls3Bv5ggYV!U5xM)dLimd(m#l^Sm4y>uk&858oJoy&@6_kX6Vb~1Z) z<*}09!-`=pke+9PCF3T{VTEidJKsg%;)m#QtQ=N6lRh+Z1u)^dhWeFXRp;MREnh{IJDD*>3!p6rrH>q#i@HoPyS)FJ$}!&Iv353a z-@SGjFuWL8E(N9+0ozZZr@4^$?^y1t0`wAIjWL$H3SE};bhcf;53j7;RW3Q@pUF|A zePN66HJ?3q^KQ#0G2YiEew}FVtnU_SkLdaiJY1%=qP+b>!MQ^8O4!1(n~B9}g9ge6 zw^A~8S>JvbnRfQ7Se+ZuyA;x1y5&E4$o8K^53~qc{vNbp72g)#UfCgdixx`1;a+!b z()(4FSUy4q`9HMw7Qe}f=ah@BL+jV^anUpP^Z0V{`v%7rtM$AaeYbK?$qz>Ul}orb zQt_Gj6}E{O&Bm{5|8wZ)ZMrknqG1)DzJ0Ni+x=zyZX9~{BIQNn+Mn`fJ2dhAZ;>ZB zHir1QGVZNf`r1?8Ez9pJFLGef_(q~Vk1sNN*}@|EKsQ9n?l=ToHI796(Kw!`PTB9| zpR92_ID~P;!_^<_bKBv8{TV|(biwdh{IV>a1v$~<{V*G1ewg+0&BE7&`Yk_9Klk$S z0`8AgaIJOpySp79tudU9oORd-uIl+qdLBD(e7&dekmW0$6KwRY%X(0JpE_gbkM_?g zAb7j?al#XgC;nNY`3P!04v_EVbR*=TOPAPo0W>K9(Q;*%(eG$`SAQ-pC%{ zhDS5^w%(wBUIOjw1q11TCXwqO{fU($MSJ@L&@M-p5Jc{X@=kl`c%9I4e2Xkzm&SWb z{^;LEqIUlgJ|`b_@wy)(cZk;|;&U%!TN$b@YH&W6IA@Ntrt8sTYc0RO5}5J7_Oh

wzH3IHXpEV@q|nS*G6q>R>NP@-d=nqh48x8W8T**krj?14uUdvpP``gHtd|OBV zR9C|9kB|Ws*#7l1(7#Fc_e-8WK2~@!WnJ(&MU(G4)(aKcrX~_4y^@#I4vA+WaDI%7(EF-ls9g z+x=?9FIjtnf!3cbYxei`a^i^YKGZd~>&BlWZ%Vo_|5lWAu18^msY+TWz#iGgKNG#~xY|Ts?Y}Q^{HKH;8n@z*pV?#k zsJ;STzDggq@oR1>E4s(thrQryE+o#0oV#CB{NI>J5%c1~7glq#7hIQLYb#^YJLGlC z_r39Q*6R>$4TXGDv&SqskuPmLPXH4r`p3S3og3e6A9iP_yzr`mep|G+T0X4Ftj~e|n9!*m^2My;H&NW7?5pB0 zmFFF=6^+!jWVCx0DV}P4#J97HwME#uSB9;0LZLjDu9&$w|KV% z!42Vo{J|?-eBNN+@d$m_exOoo)+`SBIA1#N6K2?gN6;u4^NJ^Y>q6O&mD8zVr}hM& zgy+1^{Iq43cRaM!2rr`E>|}iCc{cuA=@tw8Dn$T)k2)a-{1q;UZb-UJczI38TeOGb)Y1SoK^x2Q{6tE$^XEQ5BCCW zHx=~fV#Y05plBlN(@c9-?H6qegr{LIS->9Tkms%k7MbLnXaB1`iO+l218LkZ=rO|j zu9N+*>Rt>!-rv>pnBWmszQO^y2*yM%vF1m)1p3Ds=&UTwt?XU;P2^kZoIRO^zp-HK z^Wm_<+jloEH zr<7mL@!8b*5{}Pmd=7{BGTu^T=aDCZ6}kE1;Pg%4R6}1{kYA?6;Byl2(KDZg_t}xh zfy?8-O3~<>9?z{|K+K_wL=x56=_G%0!zq%QJChnh!!K8-g&$wU`k2ei5n7j&1 z^xZLFqV@3dzX2Y*&l-=pJzC@UW~?yMW(!@Z32zrlwIF?_h6 z{#nm}gT_&_!6<2A{CE905MIpqHP$q9;{($f>obi1I>x`AXVcAZJa8?~9%B5z0e*ME z!_H@X%b<1HJTElS*-3{+ox=N7$BW{O!#tJK2s{!e!OQuK9jTQ<6z&On0^=$ z;lQJDaCiDwsbf&d9)WW{w zkCTngSI_5sSNZo~Qw%cB0KPvXH+I*u&S$qA2$bY?d)G)e)^a}kR?|4Wh4R`5%HL0V z8F&7=j;qgO-^`hFdu_QkUjE7cZ1O%M$=v6q?-eP@`$o}@ ze>c$iRp?AZ^%WhQ@%lm$?~8a(u86}`?CG^9kIy|u-RLb7?~&^a&icGzSy_`RADm~+uyzCjDvE?@ z9H-Ag=4oTUZ0mD4hn+o<dvC+A#(9sM5ud&t)i#D?C3&O9(2 zc{c-_%1Uxs(e_12=mdz9y}(-6Ig$7NZuhs680LpqE0@3%8shsxTMfN^vGY=74YTEO zw+?D~27c?5Gc6sIbdCM-LVjL;aN@mJjW95@;;9t>rujz47P?6K3i>7=zZ&`?J@b(G z@DTL@iq{H|>%`}egEjtsKEsA}5pYrag2hQ(`&j>8ex*A__qN~ZTRe<3zws~{&UZ#~ zaOu~7>D`i&XVD~LeP&p;ovdYMc&y)O6fQaSDp%{pz+d|uH(a%^6kKgwu<1i0*b1)) zv+rxYu;zd2ta_y$d-}+o9Y-Byt%D1_xo?*6Z&ZLw1GZ+TAy$4@@`4A-?MPYux-8CtIy^7eehK>NFHZ! z1Xu&o1IljL#Pt?(?g^HnJMsIaQ}Kyb-{YSPhhOFSZG*hq&GX7Z-U%;$MBnxOO_r_)pB2`NTW=Turi1A`1TQBC z#n{Ws!t?OI_x&&sZbzodI4!tzdUD{=^V#>1|K@PJMf1l+OrCE5rvB5=gF*XL7T@sy z9B-y=D)gp=eBg4!8)YR8>=i5MPxI*DQt{X`ITyT*d!IE9@p3zti`I4p^j~G?dOe|L z;3z%c49dIfX^quW$n%!odL(!C)dPN8^_VGk9VbyX2wav^{^)H^j`=$~=XUDx+Nr_U4d z;bSB{&!zMQnfj+bbEd_=YM9T5SZAyFUC*y?iW!FP?sD{`?FDrZ$*@seNTK>zo*CbNBoq%k45`~^hZ2f{d)|a zrf=f;X@l_Bl2dGX=4R;`;>tBcCo3t}C(rcli$y0@fBZK=&UbfU;P`CB!@!FFfTp;4 zYe4I7GI=mZ&d4?V`MLPPEV~67vyA@VOTV;FSKfNd&lctackH$c zk6W(@gj>+_h`w#2-)>r%#(VLwDxQlL{$&Pdexnz2Xrb!zfdlR_Y5eyBQ`u8RFFnx9 zHt4T$j`3s*dspo{tDswN|H-0RyHwVhk5=FtgjTrcOm<<(G_p+zpS%{`R}K@&zI|}7 z01j^27Sn_EuBHD?T6M#ufwaoKj{i5>zGzY(-P-dD<;TWuQ8%TJZrwSCEJqPbpCpq{c<}mEk z>|ZC(6hHPwgpctyTFEYwYh*W_`Zw9evLh$aUI2V-V_%~Ci`m1q!Dkzgb&Ri;;j>5% z&O^%e7YJLt!zCAL9nR?Ps{Jhg!VTFAqNxjujd}iD!+4RYid$2Kf_ zd<(WK(TAECeQ1R)X)K>nJF)z7PWgdr6#H#bptsMaudH96PiAkbJ%eDUzF$b4j=c5T zxH04~hM>nDLolW61Y_u1FPcBmBYSN9pAYnliqWMCY@MQi*bf~Yiq>lsx~_R_YLJj z1OJAuuG;_MKfWHTj+o5ouEXG5rGv=$%E@!&LLa(7^nv9b;s?zL>$dS%2g!q)T3Wny z#Qfr^f0a*z5m^b%iZb36^yRbsp9-GpUU|`rxvoeFgzHkjT)Zt~9dk+?JGfZxDBghuHRC|NM`)olV=XztL5D zknwA*vU5A*|2y;UjQ=gJ>%9L{THy)KPc%Mcs-GG!AzLD6{w{c-1xKNQ|6tBoMi|w)Xwb#GAWrNvF?gNX)hz~c#?QP4TVaphc zTju@qgoJ$LBkNfhtLL{9`gp>8XRN@ucdWo&*T$4S86ZmhgM0iHvGE_C{)_7q`Nm6j zv}ifye)68pGsM$(K+k7O4n)_6Zg-Ak+9LkX=YKW-hJSMIA$+p4{uRhd576E+3n0$U2eOoQ`QhQ{NPCar(y`+TXXAa@U`Trso)ePURbrJnuIGyYhX3U6M5$B-^9}cCn|m z?zPV}2BE)#v1tB&`nH?#eJZYhHoYevJ6Od!8|XLD9~Vy`Q?9V-x#!b19d>xT#lx$; z#1Um+9~lNs91czNhT2{GXzU@I6WK%B_S$+z#g1$kkFN1i^6g-6xD~mvAtq;Lu#Vkw zroHE8f5LOgndKiNZ?8+vlnl8DyRK{>!41u&0V6nH`{*+47oYh9awI&+k{OY657543 zL-8ATPEIEuCoqTR_T}nyp1WaY(S=2)hVYf&eDk}*t@@stW9^H7YHWc{S+b|S_J28N zAg$7R_k#oM)%xv;s+e=DpX`a$Klj@I%lLt7{~xL6IbiL~-^Uwc^5EOU`_?|=(VD1> zt#9{uwVp*=&j8;ZqaJH5RFUh5wP54dV06y@u8{A3@DblxFn#S*;@`1dec`0g7l2zG zV|MH<-?94N+%UrFJ7v^g=Q~|n|FQmS9(CVSp1SYvXJuXg)~sx3#29E+4*8G9Vmlg# zjc+_Qz6szkxsTRPB7P<{B)ZhedGJTD^(lr%=fNus^QBS&CMtY|kQ(SQdH2 z-dk&sE7l9HPPA-4_)so={pZ-o$VU|QCU1XlCGV>}ytm_T^!_ir-?$X|gzW3jFU(Cw zZocbM?vaTD+zn-Ti& zQpSKyV<>F~J%L^K1BVRoG6>JTkLzK&P7mz5hwI_G9v#@VnQP&3Fek9aJqlapr<0C**Uj^8kV= z>~CqWo;6rSA9Y>EwN?MjlSAiI_dLp7op0OTuP(z@0R8CjW<(BW8I9?@uknyyZx8-$ zti>_-vy_-M4=4w-?DcJo<4}Q7bPu_fkC0pG2yCb^!z*8o?yuI117G#$l~_GZ$ z;=j$FX|?;N>)Y{h-~K%ItzY@C-0d!!Y0cwb_|}18y1pH0!*G#zV!Gl^vdl^8>VHG5 zo~+8ko$s+Xi1mMSe&x*2R>uA;^@wI@E;?q|^$550?ftQD$MJ3NcXcyEd-!gbtM7+m z-#IYb6#K57{5NUtZ~ttM`^Ts@r`?fU(qff=Y-B^YXTh9+} z>i7LJ`}><*eSRpe&w<$YnxBTfo_&Hl`j`DbKC7p8hxd{W-AA}?D^I|ivalCdV=G2R zkY4S>G4Q7$+JTobp7=ec<~^RKM4>6dkB^kqp@~B($2f*m4qhDGIne&x`b2;G^Dnx4 z#x}u2jJkQDWsf5VJ>4^#d|sRLjS-=`YR|sQ?(M1FT|H*!Zts{5@q9h2-;0mi?4H{9 zpxHeWZ62D2--q6vQ`=K()fb!>$|8s9LhAGr@9HN;zW<(pJt+H^63SOVhleyK*S^S- z&AmETV&J(u@@>$bW6xi0{Q+yc*Vm?yXY$v<=X%W+&qEVrf7r=0?TOvxa$V&fqa3;D z8hb7_OL}^e!!6VBFH0s5`X|jb$UC+TgYrRawQc2!^UjI3v^qGA=eFfP1 zo$uG!-wOxz&G9yL)GO`lq&M*|8i8#dd8)Vk=gc*Wp!4!yk*~3>d+2{ojB`ee>br_Q zsScHoa@NxO>^rQOqXOkXCKn*>Ix)ZV<+sD0L}#B?vgiLjej{yhI+T3m>b72Ej3LP#ujGFwpZ_O%&O$#~QmnS>3e0`=UUT1TwD&sg*|`kHL{gv&$Qt0xl^sKw zd-YBIQqJm%j)8dH{Cj1v{66!{o$o+% z7AfaNYpJy#9*=Iw5YIyQ;$zIdZ2Wdl;Y=E2mec7!P8r%bKI$xdZs}Xk zi$)1^v&J_HT9`*H3(wjMd`0aAi;AX^H~CPKF{goV((=jqlOI@&Zp`C4g1=cG zE$r{#-0HRMbJL(d6_($y5&8tz2G{Dt&*(d4z~k&lgta0Ydwu%3MGy1tAagGKcFtv} zCuZ1vE`k#$D_Vbu{fE|#^6^YF)6=z{v?satgVedo&r<{KegT@$_F|xNzn6Lg{({`LCmx!x=X0JBesuQjUw>$V@*+upuWN(r`0wKN;PP+O7+Zmh zY?!Nd6Jt!=j%ft8*Lw_d*c+WYc&GJML)@O$QVr*iR)8~(-OlmrIFZk_{PmAO%Q~TD z??cNvXjeXS@|l->Q3HQ~AJ5Lu2<$5KV}n4CHThwGZY6%Z`6KLSx^H9MU4_o16gsvC z-9Zz3?*Qe$$GYE({-7D1eFgjNDd53ibXM`ojhv;uvEb|?;ju4VcE@yLxffB-Vb)qD zzdz)6GC6m>{5$p#AJ4>le*dU<%ZK#)YA@%q!wcLzXOhq8wDw8xoWdB-sfqENqzv(# zZ(D0e{HGoMBc9WwJz222G|z9AjG01wE_^4IdC)${<~tKDzSBAyeQ!aWo|u<_6Nun4d9G12!Cqp@xY%_izQ37RYP~Sv0nE|o{``CIC$My=unO&*Q{3VtRd=> zB+m><{$XDNuI&iphqvH!qVwmJhfA`W!#nFzXJdz-Q~cW4 zVa0Fc4KE&d)!9Wpa>2KI$Ok_ixSWzv42_S)>GYTBqt&v{oBIw_n<44CCqLF*yVZNe zlXcZsJgK!VUH7)~E1s<1d&QGl>tAJ_^3anl;a5KX8T@K3KP$O_-%XMU{Ds&wi*k$j zwvM`6k)e;K_0&2tlYB_L{0D~{vv;pQD{S=v{|nJ-(^KQOW4nWpQ_7^pHIX0d;9cn#wF+(_5-{9NvxA7bZaO$7)|{I=LvkWSTQB> z^X0Y=c3gcv`-sKYJ9a|hY=!oXUpU?NS70B$YeqMFl}7ANChM5dI4?m*TVTV~4&JbUmbx;O23wQshskX2M2K20Jvvy6Ilp25%7cGoU~ zt^~?|Ra&v(>!pG6r}?cZU0i-%hsN6e5HvBTX?_!aDa7iqw~%l4q{u$LtJv^s+S)+* z7OTy+P0-(mprg<79qp?A+bQ>C`DOF2!r$HpJbl3Paqs0j9eDQkGX__|K(@DQfbFZ$ z50f<~7*`nc&Ja8o0)KGBPGOPf-R?VNQ2j3-9? zl&NuiQD-Ld5y*~I@q7dDI&%tobeE67M0^A)XQhOYBReZMnY*H!@Q2emHqQpS^Z7M6 z$EISh(QU)a+Y7I60Wa}4bG&@%Cq~ZShi^%2-`C8Xg^^7cu+P)JBm-I@Tg;XG3g@@} zSoFJZEj0hNXW0VyS{h~A*LzC%4@Fjb_O(yI~=Iju2 zd+3y5;rBL`aqdgr=?lhl9v1xMDtu_d;Fn}c2E_udfa zv7lGFw4*Hc?k&9o$1u3#4>dmW-8-N^$P)jhU+^E=N&I8~IGPWxS%{5SI-Xx!I-cl< z_%!XTxAa@GjV*s2U(D39e$Sd;>sdyu>C&mp(Sd^j>pZY3{A`}1eR3$SY5lNgSt@hu zylc$~E`2)o&g7ldXKVxT#{Dl+N22(hqwGbFA-Bk%Kyr)rL4^&mSQGqJrl-Znnvf4J z7Ki&)_dKKp{?nQV&REqv4C~=?DNyuPmQ+MfYEGpwT}*zw390;ihrne{?V^~aMNV-lw1$~iQbW=xnBGeSI*1N zO&a;J+$7(W+^N{?zg9Rk*Dm|SO{Y*c)pttnUnygspY+5{r}1p0@3h=e?5jpjo0&VR z(4RYcCe6S%+vYL&%Kj+vp1iHZ|>~e?2-AoM&ayS zW8T!<L0_0lUf>*ytTv^bvKA z{ir%s-|5T|{_@hnu9^Y8(Oz^MJ``z2G5d7uc|P|>vQd19xk_jK^V|3v*7LQ@=XCnM zo!a3B_;^5M-L~}um1W?4GwXb$r`>TPv8Ji|aH=D*7 zRuywjQ*v2xC1;{7f+l!|Kojh;Pu!I1q6yZs|9;}8k^g3za1QH#Ds<>{)_3)8{V>YJLo4ZDR&)j8%&0V9no4ZmUF?VG> zX6{OEGFL?bmL0t!2b!|>i*B=wqC3wA*pswd#b*)QK z*RK=QC3rO_sOw2rUDqb4Ye#~*1g{^*)m26PVOL$3C8(=DL0y8^g9+-o&sEpi3F^8# zL0y7ZO@g|rU3JY&P}kZ7bqQX#CaCKRuDWs))K!_FF2QR>TwV3l{~xZp(9;Z9kC!B< zOYpidL0#v&>gqrqjbGaf6VxSml_scbuB)!s6V!ETg1Q8+q6BqebIFF!S-x6jmrmbK>e+&a+WGbJpYT1pP`&P*)k>c@xxi{JFlmew3iDW4w=F+xgV>HqYI# zt>W2XS6%lfsOw;Yy40_~B&h2ZS6yFAP}d8*cX)R`yi~G*+xF1%-0`u<&lZh$KgZUS z?Y5;RvZKmI8b(+BQMS}*0sEzLVu5J?=}tQbLd8F6OI4rTam88iclF~v^et+qxTB}` zQR=QlpO~hYDYM+g%HblYo>T*x^!d+m6vc$?`W`8mdH)w&wo zxfQv=(l4;D4*+}Fkn8lz-rkfv>$Jxnp^W`!I^VdTE$5kZmc7sD*K+zbNo#<8zwD;* z^;Uh*(^oI@dKGp24mq{A4xZKX>>2kn>aC*Qr(Dmfh!Hr`)wap`o@co3tGGYib#Er& z3mJEBojDL2qu%Gc-sg`(FCTY5KJNa&Z&|mo?^{_nW8&WHTwUjVJ@vgDS6=JfU8anD z3(vXAlz|&6)50^&`3C$(ocfx9hnp7c{_XKz80J$Cc@%o@1*<1q^)@qBogd-URX@_+ zD>(O6yjL8eb6*d>yz08or~IGX_nFuXUHA3eODEC${cz3zv)Kiqv#Te2(mwo&c!bHGPVbogtp%6>p@S@5dkb^OKDH@?H~ z!lKjnWuSjWS19{kO&lGqXRlcU9d-LFJN)D#k5XRgrUE zbUIP|e^0q$wbFrk1~4aH%j@m+IT-`YP0lp9k9=@#z)|D7dzi7M<|~25jSJDmt~VOn z%ZWij_Z_XSDLuB*QvzO4PUjqDMV_Z@_u5ZT_Y!Ox=VPPv#^5VImsFnjkD1jx_rlog z*&gNW$X-43EOc1(M`x`BfTcAj@T8ac*ZfD(?PDYB^Tm_zBJ+ZOQs`cEtz4~yvAdA&K1)y6l6!KbSS#`I>#;9@()v84}l*(ANrn@z;@{leE5Hv!40MI8FToa#fR`Wa<3inPu+W29~G=6 zANQUL^~o1`oAKZ$W95nM-w$*rjLDxsZm0q$RiAKDIOL8uE2F-DrJk7LX=r8d3N5}0=H12<(p7+38XAVE>j5{#9Z`>XBxckkoGj_(K=b}xA zr<=s8c{qEjKdp4=)Q7}qYb`0~lbweyJF=W-H8J1V$1UI3?8pM*$bA8SuGV!a*J-iq z!_N0cQt$T{xxSartiCywZ_;DmjOIF;K^y|lN9cM4dPe29Lq7SU&J1FGv=)%#jySJ; z{zW4e@|%Nv?St&=>5=bNX=F+dKv*#^0>4@JCNR)G)J`ZtrT^^ad)WBL?N z@tAQ&BkO+GGGd}F`T~BZbyvK&0AIKE-#;|L@^5=|;{+B?b{l+8dHM|#`bQjJk7V|8 z8`kpGS@U;$Z(w|gU#(gh$as8lusmYVq4KULgykq;X~9l%gi&PSs^FI&gI^#H ze$Bw|X4-z~1n_%~cm3&3j7O6jqEdW=we9feATeduvoWq`#FoWBBYtb(x48+*#d4jw z+o-~)PxU3re}<0&JbCb#ZTRv>#%zyCH?|xjW;-VX`{DQ5E0QN4dOTaUM?Zd|@@to0 zQ~J8FPq=K#=RC$Y?~AST9xu5H$%m|StywEOd>`Aew9ez(l=pFRsZF)7Cvh$Mkp=vZ zLL2nW9PzMN*=$$YQL(Z<%BoL`X+N#|Azb{8Y~3rDzxGe{Jw40%#hH_5XrR3f{}!Hm z;6vd_t~hh-ZC0FllO1PXlNrFT)@alj)f@9SEHzfG>IwvmE@R9^maQhh+3LngvFnk> zu8ms*C5u00lx$>f7i;a0GD3^MQ|pZDQAwdi*;|1|o z6wb19w5>SH@#g^%fSsRvqow~(4Yy2CQjx~InWVfoB%)Aj>*1*~l-Wfa>e%;4d!|8$UH^>98 zIE>f$t&^?vVgCr@mE{+hN4z&0FD<{o_%{EQ%QN#opM0wE6la)z-sCqw)7V*`89mv! zUa%4`WKX-LCgmb)@BOTJO1gc`ejmT#?8sJhl8MeoX)pMdWxIZBRwKN|mIu0SOyE0| z8ra7almaIG#$cH`?Jwo;P4xj(=(Iz$*<4gIuKL7Xa>&pDxG-F<_*5}9IgKgfl zC28x-iw|X(rx&e@JXqAd?!`m5<(*zwpFS}y*%(Fa<^G11-(6a0)ZLW9nI3QQZh@Jx zdZW2xNHkpPD)GR5K$(ehTf6 zY@BdvzA^e+t$APi$TH^`UphZoxz&^2DI>?Q`mlOJcIZvox}LT&>BqqKhSA<|+RNCFa_NM$Fw>SI$ti4tERj=|RqX5sTp0al@V&o1Vv3c;mH2^PXe= zi$@vzijs|eyOWJ~u$8jM2<~4sIw^E$C1(SezD*zFKZq<-fsaDaZ05I$-#z?p;CC;- z)$@J$6XlsB->E{Ls)3$wtfQ}dzxvwBd8?0p<;5kdR)6U_Puiw$nj`o9&@^t>^Cjri zRwoB;|7Pci{?E)sw{PlKhWljKH|D2!?{n3`eX{G`yqxzVUGKS19(g+qCR79+&l?4f z;{TyMW4AKSXI@V|)%S%TEKMJtl<7O>9pQ^Uw6yKjhMO|D7wz~M^r9GA(aPTUaZmQ9 z7Ej=IgEiWc!tY7LE->y4XUaFp`17XLd+^OruY~QGZ>1bo!UL zZ;@EvihBEY1AU9{m-_n8_if6*t#9Z3oBMY2=-D6Hw_BOhhS;1o^v>x(xcUEyzBR<= zv>|Rz8>&CuQ3UNTg7z0d`;{+#IkdeeH4xGn@^1gAt{eOIQg6+&{i9y`0eK;kY>?{BqJlaKeE!_e2BzlTiT}yUrYX5A`JFm$O78M;`MGuYQdNDTAlDo=YS%)1 zSG*%H+EvNkN#A`I-=VMa-S@^#&b^cG?&SCLOZ{-t!ardaU-rThfyQ zqx{t2{@(affmJ?(@|-obdKKk-*`pfM$Bk+%qQH?(Ts77bsJy<+p2XQ3I1LMR7 zI{Wa4*}o~iBse7yi=`PKdGTT5d}6Udbrz2wA2Fc~+kS{#hL6=2^cT_(7D3nKw=JJ& z*3=5Pcw!R9MV25x=nO~B4SB2#nRW|4)$!lgXsh$mBHd)DRTF*fXcSC=!+Lg}^Y#>yXIp!32vh_l<0(fQi>eVlo<^kKlU z9}L0ed6e%5L*~^DLp$~-@z@mW^7FUQg`*RA{rfhIuVhc7Sf1rJtjSep!+Iim!FX6Z zV}F!6Xk&im11TNk2g$~kLh#0Iud4^vh16}sdVD|Kw*xCDHm8zuHqK16a3&tsk8%dl z{e!@}H-4uYKhNn&=wOnO!3Um^=d6T+Ah`a|+|375-WiI45h zj{M;F1NB^U6O{XotK1VYeb(qd+WIi*gwE%GHUBllb0g6oZ zD$+Rz4mj)mm)9Pzm5;C1;~ai>kgv(l8W`)e@6yMubG5&d_NCjpiE`4}jXMqh>{y-G zP-ky_tW~WapE-woEYAG&U(?1p({laiOt<3t#?S}VGx;rIeCU(bVkL9-5;5KJ>oA}F zxm73nUWZO-zb?F70A5NrwkyS2`@Q&>4L&-#?sBe5E*f{5(Wv%h54i&TYe-<{Xi&ec zHtf94>X-T?TcueY1a`w|vm^7-wJ1jqi88_DsEX+OTl)VoI;yarL?A2G3!S zbT+y{J4Pzr-tFXfH^H|`)#cL5RLw~6Y(>n*@@c^;W@EwcJO0}m?6oz}^EC9)(tB7r zlVbE-YxIj>29FuPUwi70h_`ZVhmI{>_KCsP)%!6&V&$&a8I;aCt2Y9TbxCE#%A0|m za_L6$4n;@UdPBv#$zNLb%h8JWL~lc$kZ$y|*};^+^^zy^V=|?+$Nls28??WTo?>*j z7AP;mxXA54>#jBD`Exx@#Ufj$L)LHn_R%F$} z8Rq7?5x!4_(W{)2tGH2}FXh~qAxGr%t+Bak#BJF6T8Y>4Bn|tFcvIk|e5N$2Hn>_uqS)?7guQjKE@;DH%8o`Iq?MX^Y680KY1+=+JQX8ViG^6$`6M z*B?(Ra_mo)dpsTMzmmjRIa!gsZ+JW2yP>;w{mJNMI45)paGVdEB(q=2`4@%2wuQV$ z$4)UicVBC)Sx8JoA?^8*+sCb|c+u=CoY96p%~w_w8uN!+ch03e=P8sdUubk$IXCi* zZiglwqrIlz1&MX&vxT)_yF6gcmwl#^Vz}0ECV0!wG>1*|H7j_ z`?mWBN-8@uBeY`w`L`NKH4nuG0XXSHWv1OL*q zaMs7ta$+wVyqC^v8*R*fADLjxL5+7zq=B(Cc(2&Gk@Z$IJkV$a0$ql$E;G8o=q8@C zv59!rXn9R(bm2K&(S_cw=rr#4RF~G=@Z0@`!~gc~WYZU^#}_5PiF2{2w;CN^*C^uS ziAyk*b8vH!_58p}GjwYXxrNe=xt?-j7x-8ElHb}}SxeJx+2LOL@h0*0%2lHNoA`*& ze8kol3I1AR%-R0czv3C+DCYDyCHBW(%gl{htAjqL13k=b(=jZK&IHU9q42gK*zVZk2;0`p02veJc*KX&bmp zf2z{YDXM|)lGmh>Tm~h|Z_>8jTXe9%2)#x*$(?UK!@N+oA_0wU7M(%fb8MU*U@f^- zb7r&}p$yh|_u5Oq(PiN3#o+8x;zlk)9!<9NjZXeaU!93Q!aVFpCXj9|p62^$qtL)c znV=6wIh4QolO_@idJMrp8kG!?FM2As_}2W6Mc5uHa*w6%ZUtU zJ?i`#q*r8Sbc(=kC}SLI{Iu${X%BORjotQNck;9L+1uP_5y@T^Zx?mv(3o6z;xXT) z@#5c5y^!%v_JsPzJ0`M^_*nJ5j`ipCUNi+B5p-Q(FGns|wc@KIlWYXb{_?=h#p-8?Vf=t_|i+&*H~c zar`qKLs}24GY5vMCsCY>@*Oz)QN@Qja@{N5m`rY_TQt_kzAcZvV>@f7hxMp)UmCdH zt!r#bR!olJ+5R{<{NL}cdvT7pcFs1=bKZ~?_6^I~R6`7t(#Z97 zGxYwk$;LaZ$7QRiZx!bkOhc|c-z?nOft@or4SvNQC_a`%<2jFdMau?@Z&uz7?Js0c z7CtK`Jdod0`+f#{?JR85d@s3xy-{bS@_jZ>2#Ig_3~06J*Y)MyMv~EOjSU_nUL-yw z9%R6SjNsS!|2qH30MNgtg1)>nAD$$8mCgjui5%s+VKn=ZG*^CCpw`|G4^b5>Sp_Q*CJtX?LbEz3_M1E6W>pY(AJ)WGy z(353RPtLQ!)Q%^{c9q|Akap^rhc@OX4ZBmAxlrAji*)AVW!m!P+jH?7=HgoBVhMAx z8hw%GqE2%`AEI$G-E!vcMsf##kN;bjl9P#X3k`a#iJ#ZNlE4a%8OHyJuD-e#~? z#xsAwrFEqd@}p~VVE1De>@Gjn^Vkmh+03(3$9C8D>`fxq+X#5~wiaAwCBUw4L*p^04rvjWKIptUSPT(Sq$)XbrV(+zAsDz(l`?g>gV9(GCyKUst_1ds&11Egl<@dDz)$_CP{rA6Xu1kAR z?dEZQ@IhjVKT92-!=Lp4--sW7lCeo9c%StYott?6|9-Hmb}IAV@cpyHMVwn!MEw!Q zv?P3P_CTOT?RCQIg3=X9%SPbkC3b_ zU0n?@YhW$(&J}RaU=FlKgWzs7c8;QjyY#)aAO?Twz`&D)XO8eL(fX#UO~bq6@B{_m`jYXp<>fj*johvk1GddPTf zo9;;+z97Du?33l|C45~E&1tCqkB&6Tx%L0eqZIc!&^J@__VRvPhgNvJXO8wmvx3k| ze)aCOmz2lQzP=S57WRXZo&vK(dNOQY7v-U=qkYSE{mU2egEf6;^sTf0^1S#9`@|AE zM`Cv5G3t(%1Ap4OyNcYg$9py#nP9n#v*Ej!G#T;KBL)_wFmGZxM6y zD0SbG>Dz>UZnX4%;;rjF*@ssEw+iq#sI!5P+kNIyJI(^%6wwD~pK=7eXej@Wd6kB7 zMjdsXx(psh|A#^cwHHc+x9tbIYV+u;=6eb7O@uSSzx6D;sNTCpbk5oP;5UW**W7Sg zeN6T{Ez82|fcSFMZ(Yk5)n_g&F~+A{EO{UplLfV&bsrx~5%X0b$C)kDWKRinzJ$DT z>oj}Lb^|K|o=H6ZXyqByeFdnx?b2FvtXAW2fL%duHxiC=PmF^FJoD?)d*EU z1M-Zt!$IcEfTlNqm#VjhU!6rET2pv?Wrt`<1+*j){O$*S&+}gaZ5+xxR6uiwQqGoj z`_5PbN3w+r-kI!av580z81GB%&V|*t_H2LZ9CG~UkmLUtbXxVKm4NpLSzlgkER3%$ zk9cEXAU%@&sgJq*s4d-*_TKnXM^niIb8&fCIpAM{w$f(EEdvSX^~~^b)=Qmt%MAQj zEIEjr1di-hhmW)PnXk#yVKyPRH3dtv@prE97@bl2F%BmTg%T`#-$jXCTvX z9;@oIeUSR_Q0w*<`sS`LQ5kk=E6Hl*1v0(bg#XDHaJ~wtw9dl+%=fmFfN4trmv*G|HgTyvnTB*^-TrW)Hfvc}r8 zkaftL6RqA*8YFj4w7R-M!)N8z2hz4CxrB0gUEhFMa10vINuQ;yx`DEmIs*2~1?@gVT_YxlEwxWnwVG>$rQ4b_upsC^dC(A&2Y2NGsHt<~g} zB&SuwyTqidBDlvKhVB2Bt2Hb{flt(m3r`toTR>_38V8&kCl_w!rA}QbEDDlj+^V` zEB$foneY?dhU{kNS^o;O9$5&RcyPaDtOjt-H$Q;fWak~$e26EyZSIafOW!1Vo=89E zA(ABzjNa zN#kMPsm8-2Pc^<>cR?6`+x$N?&-YKNxWEh;HyD%ZGt;mKrDLaAY^F_<4N7~ND0@2P zrFG@Dwd5n^!F2Lo2B4=}(~DT68ar_MsTKFey4IR@=xH14n!HHc+gQiK;kNhW@5TI~ zM-?Bl=WS7M-pE8e)5)D^>t#6C;Je-}@jUY=d#F}mpxocTepmV4X?KWxFA={>)DOvn z^39OEEcqMU8r{m69`u`A)VA&SkQMPrKTF$&bP+E3xr({B#-3)czeMd)=TgmcJRf!I zj=QNJ9DTu8HNxhNKBF`1ZKI?qOpaPV=M^bO?S`sS(VB)c6$_)h+x>Z_TSw^Rc=Ej+ zP%iR0LHzUkuh;Cz;}3Fn7`b-?ChHZtRwKWD>RxW;oU1DJ({H!#P~&jlf0SQ;FaE^w zH}_d=d5rrge*H(;SN3f4lt5#xyf}r#w3^8^6Dpt!8&+@*%P$8%PYwAZmhXC?&-!-G z5ODSr5!%vzV$yfDpRo0+y}#N=fWs#HWk0fST>_o515>}s#rWfEyNLU}&pvNGNMBdH zOFlaK{y#jo?uj2@U<7N)vi77bc==r&ujO0M^J}%Bc2zzv{82a1=JDcTC}@KG>&J|AB}()2Fd?4)uMCc z3837#^?`fd|4QJV3da8!zbbb?dZRkZ-tm5E)g2wB8}2w-n*UPW1W!H==pp$w`7X3F3^ETNH+U5~v}FR!ntjTRBNkav@l(H&h@2Yy+{EC@mE` z<)ZzXM46ekQl(RV2C*#`r&ADbZD&jX$3V1-3M7!2@B6dQ+2JH42-N!e{eFL}*S@X2 zF3);y>simU*65ynpj<8!eKJ8ybcZJ59_HoL}y z3oLk+_O={tpZiwxQs=rys5kSB^^WPO_u!Dub(vB1j@JHD51VzF)T??@s7F48C#Xm5 zNRHI|xY|LTE%>hO=S;bB>aF5@t7_sZ;O||{vyJV@wYja=8rw8aRU3=RQ^S5stxp&Y z*!Q-zVwY@6=j>sUw?g%Gc289O&e`ApB-qJzOZobq zkKB?BsZyJGkB+kS4aP-fJQ^v(gRRhDKD>&tJxU(Ml$l)-c^_mxY#{3k%?7R0I^`H0 zePSx{aaqLw&r^1^oENNgj(dN!T}=LSvjv6MJ)1d~<814gb&hqs*eMwtuIGv$N9=m< zk9F*Ne>A=nUej%k@LtZf#V@L9T8zhNh#hS<#A&Y28k~2dyCD-fTmuex(@y!oX|A7% z-$K^=R=f8bvF)BrBdq7H=;z?w2bk}< z6Iq*2XRZiOP=5S|_AdT$_qvgVR)goCgAFZf)K*X9zX6k$&AAOO)^xRY@}&K2HMlrm zPI&Qzo+-Z-JDsazIq!(Y;@$BT?8odH-B!i^g01P`d}nvB`o^g)`mFyv&|EzIJWP8J zu?ItHk#VGiJ^K@gf2jl>-+lM~m;cQiT>B0DgNC^V*M^7}F^$2si+J~@8Q&HNSSOm( z=9~zmf9b@7UH89Sm1UhM!hVrJpB4@C)F#r_e=rkk=a810iEX);U322y&u>V1)AY6# zS6_P~1OM(s>>CM3mOfNznst{}8+F%`-ghT7?x{BR1lCwrAK{wL^@Y{e)&EQSnQ86` z_>8L`_ZnAkFYsQnJ@D4-2lB#)V}JMe(j z86*W_MlTA*a@Gy8K1JAJpMox;$7au?;c?k4T4b!`uk$?PtNRmNqvzxD?0WQ(PtbAbKpnyJKcN2GBUZb>n>oF z$G+OU``kU@`>^aB(KwT5={W1+ji~YkkcmGITg4rfw*rozY`q`D4DD?O zHfmq^-AH?nmgZVx%K_Rg-_@IskmvHIY5TG91u1(!`@IJ#e?MghsY5z^^W~Ho_s@*S zSH{<`{HoR!d&L!*tiyow#1w_ku{m~QE_4w@H~LMz^z|L<-D}^y{Fz=cs{@_!(Y#j}oTe~xpJcA!6u>JGkKiq4&Cj;h#=U;AWWf1G)m_`3W& z_M`6|;90UEmT|589Q&zrZqshsZtJJ;p&3v7jy=$g>?|{eHS*xYikVu$chmIlZbnCa z37>!&(53v=M^O%IQG1;_$FVk*!uvAXQWN8;EzOnQ5nZ>=6B*NZE%n~D`6||llXs7z z4(I=J>baX(%E|1}-poABM9xkd$$qWKkB`G$G!G4^^Ha~ONQOwZ_!h1SZO+izNF?m4yIcyhq?#wE#|8^<{g zOK3xDMvSfZne@5Z=0(sy?@l=PHjGC#ri1@Mbg<0#m%fa@%Rd~V&%RYwd*A`1GdRp5 zRxxd`{8%I##6vpuKjqB26CTEyFCA|VZRlPeLvoW`4W<7lw}E)BwshoL@-0RKo25tg zwZ=YJjn7Z@%Bcv*VP;)sqyEGy@#D7}zj$W5m1fq7_aA!9+tBt+UwiW?%7OkkpR41*!}$%8Py21zG_tXr zIj|4jIgWHSsgl%u?b)p>ElEZ{K_|*`%g6sz&EZA(?F^@(*EZT=-_G2 z7pscU&29Pzx(GoRrQn6=ri6d)xFNTpzjb8M#l{skw{Kk$O&8&{WV6p&@<;UL4sghs zKRNrJMfbUtxRuw*XTOWIqxEF>R`KGtanBMPWpeh13;yVaPsU)kh{ZN?>ijpIQSb1; z(|8`|el$M@?J>W8o3lJyu|d=@2UFY+{K)nf)AxgsZ>_8Y>N)<0oE1Zyk$9z^fcq-P!#>){W^%l~ck{pf9hu{NOEQz<577tJqxytTVcwe5xEjCU`=B@5#|d1v zZBoq7{vXOY#=6U^sm%LUpzr;c*^nq&TM4bHoh8s(hFSN&2cR|l8C2eDtQ+h9Uqt>r z_BrF}*VrfH?(XghAMw3v(2nsdblx>_Cx$1DL<~rP{dwriuFceeZ?gXXL!=M8*mF^Q zeK&UVik9p;Xmn^fdoE^qeYF9Tvk+;YZ%44Rl{RT!ARd>0Cwz(aweW6-b3aOU^ihq6 zcutG(xI>%)A^w&Pf3x6k1+N57@i+c|LH|<5usgcfT}Zhpq@(nannZSNNxx$ra?uynudqJhfw(hdKY3l2`e(UbKWf zHh*#(M-IE!t$zm`c<0SQ4M(pX-q7(_Y(x8#F%2g^bT@p+8gSXa<~FboMqB!!?!rsH zY&1;1#caqday4X^xf`aY8LY{N=fyt8$π$f+9Muelu@LQZ8Or!pgQN@s(tW}a2< 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¬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 q=v@ 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 C{vdczov-mxU#;+3nQK1s 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 z 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

E+ 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 z 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

7Fk>T@sv_V-z;G{ka7(wL#PRKM{6Z0rIZ?q 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

ax3nhJzOp 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 ViewController func makeComposeController(context: AccountContext) -> ViewController func makeChatListController(context: AccountContext, location: ChatListControllerLocation, controlsHistoryPreload: Bool, hideNetworkActivityStatus: Bool, previewing: Bool, enableDebugActions: Bool) -> ChatListController - func makeChatController(context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, botStart: ChatControllerInitialBotStart?, mode: ChatControllerPresentationMode) -> ChatController + func makeChatController(context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, botStart: ChatControllerInitialBotStart?, mode: ChatControllerPresentationMode, params: ChatControllerParams?) -> ChatController func makeChatHistoryListNode( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal), @@ -976,7 +998,7 @@ public protocol SharedAccountContext: AnyObject { func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController - func makeStorySearchController(context: AccountContext, query: String) -> ViewController + func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController func makeArchiveSettingsController(context: AccountContext) -> ViewController func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController @@ -1049,9 +1071,12 @@ public protocol SharedAccountContext: AnyObject { func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, requiredStars: Int64?, completion: @escaping (Int64) -> Void) -> ViewController - func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController - func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction) -> ViewController + func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController + func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController + func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController + func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController + func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController func makeDebugSettingsController(context: AccountContext?) -> ViewController? diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 76892927342..f79b6ff0ff9 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -34,6 +34,7 @@ public final class ChatMessageItemAssociatedData: Equatable { public let automaticDownloadPeerType: MediaAutoDownloadPeerType public let automaticDownloadPeerId: EnginePeer.Id? public let automaticDownloadNetworkType: MediaAutoDownloadNetworkType + public let preferredStoryHighQuality: Bool public let isRecentActions: Bool public let subject: ChatControllerSubject? public let contactsPeerIds: Set @@ -66,6 +67,7 @@ public final class ChatMessageItemAssociatedData: Equatable { automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadPeerId: EnginePeer.Id?, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, + preferredStoryHighQuality: Bool = false, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set = Set(), @@ -97,6 +99,7 @@ public final class ChatMessageItemAssociatedData: Equatable { self.automaticDownloadPeerType = automaticDownloadPeerType self.automaticDownloadPeerId = automaticDownloadPeerId self.automaticDownloadNetworkType = automaticDownloadNetworkType + self.preferredStoryHighQuality = preferredStoryHighQuality self.isRecentActions = isRecentActions self.subject = subject self.contactsPeerIds = contactsPeerIds @@ -136,6 +139,9 @@ public final class ChatMessageItemAssociatedData: Equatable { if lhs.automaticDownloadNetworkType != rhs.automaticDownloadNetworkType { return false } + if lhs.preferredStoryHighQuality != rhs.preferredStoryHighQuality { + return false + } if lhs.isRecentActions != rhs.isRecentActions { return false } @@ -227,6 +233,7 @@ public extension ChatMessageItemAssociatedData { public enum ChatControllerInteractionLongTapAction { case url(String) + case phone(String) case mention(String) case peerMention(EnginePeer.Id, String) case command(String) @@ -288,11 +295,13 @@ public struct ChatControllerInitialBotAppStart { public let botApp: BotApp public let payload: String? public let justInstalled: Bool + public let compact: Bool - public init(botApp: BotApp, payload: String?, justInstalled: Bool) { + public init(botApp: BotApp, payload: String?, justInstalled: Bool, compact: Bool) { self.botApp = botApp self.payload = payload self.justInstalled = justInstalled + self.compact = compact } } @@ -730,9 +739,11 @@ public enum ChatControllerSubject: Equatable { public struct Link: Equatable { public var options: Signal + public var isCentered: Bool - public init(options: Signal) { + public init(options: Signal, isCentered: Bool) { self.options = options + self.isCentered = isCentered } public static func ==(lhs: Link, rhs: Link) -> Bool { @@ -1071,6 +1082,7 @@ public enum FileMediaResourceMediaStatus: Equatable { } public protocol ChatMessageItemNodeProtocol: ListViewItemNode { + func makeProgress() -> Promise? func targetReactionView(value: MessageReaction.Reaction) -> UIView? func targetForStoryTransition(id: StoryId) -> UIView? func contentFrame() -> CGRect diff --git a/submodules/AccountContext/Sources/GalleryController.swift b/submodules/AccountContext/Sources/GalleryController.swift index cdaccb9ee08..33b8a49c8ff 100644 --- a/submodules/AccountContext/Sources/GalleryController.swift +++ b/submodules/AccountContext/Sources/GalleryController.swift @@ -6,7 +6,7 @@ import TelegramCore public enum GalleryControllerItemSource { case peerMessagesAtId(messageId: MessageId, chatLocation: ChatLocation, customTag: MemoryBuffer?, chatLocationContextHolder: Atomic) - case standaloneMessage(Message) + case standaloneMessage(Message, Int?) case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId, loadMore: (() -> Void)?) } diff --git a/submodules/AccountContext/Sources/OpenChatMessage.swift b/submodules/AccountContext/Sources/OpenChatMessage.swift index 61d922255af..ade6e84c968 100644 --- a/submodules/AccountContext/Sources/OpenChatMessage.swift +++ b/submodules/AccountContext/Sources/OpenChatMessage.swift @@ -25,6 +25,7 @@ public final class OpenChatMessageParams { public let chatFilterTag: MemoryBuffer? public let chatLocationContextHolder: Atomic? public let message: Message + public let mediaIndex: Int? public let standalone: Bool public let reverseMessageGalleryOrder: Bool public let mode: ChatControllerInteractionOpenMessageMode @@ -55,6 +56,7 @@ public final class OpenChatMessageParams { chatFilterTag: MemoryBuffer?, chatLocationContextHolder: Atomic?, message: Message, + mediaIndex: Int? = nil, standalone: Bool, reverseMessageGalleryOrder: Bool, mode: ChatControllerInteractionOpenMessageMode = .default, @@ -84,6 +86,7 @@ public final class OpenChatMessageParams { self.chatFilterTag = chatFilterTag self.chatLocationContextHolder = chatLocationContextHolder self.message = message + self.mediaIndex = mediaIndex self.standalone = standalone self.reverseMessageGalleryOrder = reverseMessageGalleryOrder self.mode = mode diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index d847a744d40..46d61341115 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -31,6 +31,7 @@ public enum PremiumIntroSource { case storiesExpirationDurations case storiesSuggestedReactions case storiesHigherQuality + case storiesLinks case channelBoost(EnginePeer.Id) case nameColor case similarChannels @@ -40,6 +41,7 @@ public enum PremiumIntroSource { case messageTags case folderTags case animatedEmoji + case messageEffects } public enum PremiumGiftSource: Equatable { @@ -74,6 +76,7 @@ public enum PremiumDemoSubject { case messagePrivacy case folderTags case business + case messageEffects case businessLocation case businessHours diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index ef5cd03b33e..c232006fb21 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -419,6 +419,8 @@ public protocol PresentationGroupCall: AnyObject { var memberEvents: Signal { get } var reconnectedAsEvents: Signal { get } + var onMutedSpeechActivityDetected: ((Bool) -> Void)? { get set } + func toggleScheduledSubscription(_ subscribe: Bool) func schedule(timestamp: Int32) func startScheduled() diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputActionButtonsNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputActionButtonsNode.swift index 045e0a6459f..16b413b6ae4 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputActionButtonsNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputActionButtonsNode.swift @@ -67,10 +67,10 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode, ChatSendMessage } } else { if highlighted { - let transition: Transition = .easeInOut(duration: 0.4) + let transition: ComponentTransition = .easeInOut(duration: 0.4) transition.setScale(layer: strongSelf.sendContainerNode.layer, scale: 0.75) } else { - let transition: Transition = .easeInOut(duration: 0.25) + let transition: ComponentTransition = .easeInOut(duration: 0.25) transition.setScale(layer: strongSelf.sendContainerNode.layer, scale: 1.0) } } diff --git a/submodules/AttachmentUI/BUILD b/submodules/AttachmentUI/BUILD index aceb5e800bb..e1216ca803a 100644 --- a/submodules/AttachmentUI/BUILD +++ b/submodules/AttachmentUI/BUILD @@ -41,6 +41,7 @@ swift_library( "//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView", "//submodules/ReactionSelectionNode", "//submodules/TelegramUI/Components/Chat/TopMessageReactions", + "//submodules/TelegramUI/Components/MinimizedContainer", ], visibility = [ "//visibility:public", diff --git a/submodules/AttachmentUI/Sources/AttachmentContainer.swift b/submodules/AttachmentUI/Sources/AttachmentContainer.swift index 6c4d3c4f857..61549339325 100644 --- a/submodules/AttachmentUI/Sources/AttachmentContainer.swift +++ b/submodules/AttachmentUI/Sources/AttachmentContainer.swift @@ -7,6 +7,7 @@ import Display import DirectionalPanGesture import TelegramPresentationData import MapKit +import WebKit private let overflowInset: CGFloat = 0.0 @@ -34,18 +35,19 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { private(set) var dismissProgress: CGFloat = 0.0 var isReadyUpdated: (() -> Void)? var updateDismissProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)? - var interactivelyDismissed: (() -> Void)? + var interactivelyDismissed: ((CGFloat) -> Bool)? var controllerRemoved: ((ViewController) -> Void)? var shouldCancelPanGesture: (() -> Bool)? var requestDismiss: (() -> Void)? - var updateModalProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + var updateModalProgress: ((CGFloat, CGFloat, CGRect, ContainedViewLayoutTransition) -> Void)? private var isUpdatingState = false private var isDismissed = false private var isInteractiveDimissEnabled = true + private let isFullSize: Bool public private(set) var isExpanded = false private var validLayout: (layout: ContainerViewLayout, controllers: [AttachmentContainable], coveredByModalTransition: CGFloat)? @@ -71,7 +73,12 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { var isPanGestureEnabled: (() -> Bool)? var onExpandAnimationCompleted: () -> Void = {} - override init() { + init(isFullSize: Bool) { + self.isFullSize = isFullSize + if isFullSize { + self.isExpanded = true + } + self.wrappingNode = ASDisplayNode() self.clipNode = ASDisplayNode() @@ -150,16 +157,29 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { } if let view = otherGestureRecognizer.view, view.description.contains("WKChildScroll") { return false -// let velocity = panGestureRecognizer.velocity(in: nil) -// if abs(velocity.x) > abs(velocity.y) * 2.0 { -// return false -// } } if let _ = otherGestureRecognizer.view?.asyncdisplaykit_node as? CollectionIndexNode { return false } return true } + if gestureRecognizer is UIPanGestureRecognizer { + func findWebViewAncestor(view: UIView?) -> WKWebView? { + guard let view else { + return nil + } + if let view = view as? WKWebView { + return view + } else if view != self.view { + return findWebViewAncestor(view: view.superview) + } else { + return nil + } + } + if let otherView = otherGestureRecognizer.view, let _ = findWebViewAncestor(view: otherView) { + return true + } + } if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UILongPressGestureRecognizer { return true } @@ -198,7 +218,7 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { let currentHitView = self.hitTest(point, with: nil) var scrollViewAndListNode = self.findScrollView(view: currentHitView) - if scrollViewAndListNode?.0.frame.height == self.frame.width { + if scrollViewAndListNode?.0.frame.height == self.frame.width || scrollViewAndListNode?.0.isDescendant(of: self.view) == false { scrollViewAndListNode = nil } let scrollView = scrollViewAndListNode?.0 @@ -250,14 +270,14 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { } } - if !self.isExpanded, translation > 40.0, let shouldCancelPanGesture = self.shouldCancelPanGesture, shouldCancelPanGesture() { + if !self.isExpanded || self.isFullSize, translation > 40.0, let shouldCancelPanGesture = self.shouldCancelPanGesture, shouldCancelPanGesture() { self.cancelPanGesture() self.requestDismiss?() return } var bounds = self.bounds - if self.isExpanded { + if self.isExpanded && !self.isFullSize { bounds.origin.y = -max(0.0, translation - edgeTopInset) } else { bounds.origin.y = -translation @@ -289,7 +309,7 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { } var bounds = self.bounds - if self.isExpanded { + if self.isExpanded && !self.isFullSize { bounds.origin.y = -max(0.0, translation - edgeTopInset) } else { bounds.origin.y = -translation @@ -306,19 +326,31 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { ignoreDismiss = true } + var minimizing = false var dismissing = false - if (bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0)) && !ignoreDismiss { - self.interactivelyDismissed?() - dismissing = true + + let thresholdOffset: CGFloat + if self.isFullSize { + thresholdOffset = -180.0 + } else { + thresholdOffset = -60.0 + } + + if (bounds.minY < thresholdOffset || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0)) && !ignoreDismiss { + if self.interactivelyDismissed?(velocity.y) == true { + dismissing = true + } else { + minimizing = true + } } else if self.isExpanded { - if velocity.y > 300.0 || offset > topInset / 2.0 { + if (velocity.y > 300.0 || offset > topInset / 2.0) && !self.isFullSize { self.isExpanded = false if let listNode = listNode { listNode.scroller.setContentOffset(CGPoint(), animated: false) } else if let scrollView = scrollView { scrollView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: -scrollView.contentInset.top), animated: false) } - + let distance = topInset - offset let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) @@ -362,7 +394,9 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { let previousBounds = bounds bounds.origin.y = 0.0 self.bounds = bounds - self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + if !minimizing { + self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } } case .cancelled: self.panGestureArguments = nil @@ -390,8 +424,8 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { return true } - func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) { - guard isExpanded != self.isExpanded else { + func update(isExpanded: Bool, force: Bool = false, transition: ContainedViewLayoutTransition) { + guard isExpanded != self.isExpanded || force else { return } self.isExpanded = isExpanded @@ -408,6 +442,7 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { } self.isUpdatingState = true + let isFirstTime = self.validLayout == nil self.validLayout = (layout, controllers, coveredByModalTransition) self.panGestureRecognizer?.isEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0) @@ -422,7 +457,7 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { } let topInset: CGFloat - if let (panInitialTopInset, panOffset, _, _) = self.panGestureArguments { + if !self.isFullSize, let (panInitialTopInset, panOffset, _, _) = self.panGestureArguments { if effectiveExpanded { topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset)) } else { @@ -435,9 +470,29 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { completion() }) - let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / defaultTopInset) - self.updateModalProgress?(modalProgress, transition) - + let modalProgress: CGFloat + if isLandscape { + modalProgress = 0.0 + } else { + if self.isFullSize, self.panGestureArguments != nil { + modalProgress = 1.0 - min(1.0, max(0.0, -1.0 * self.bounds.minY / defaultTopInset)) + } else { + modalProgress = 1.0 - topInset / defaultTopInset + } + } + + if isFirstTime { + Queue.mainQueue().justDispatch { + var transition = transition + if modalProgress == 1.0 { + transition = .animated(duration: 0.4, curve: .spring) + } + self.updateModalProgress?(modalProgress, topInset, self.bounds, transition) + } + } else { + self.updateModalProgress?(modalProgress, topInset, self.bounds, transition) + } + let containerLayout: ContainerViewLayout let containerFrame: CGRect let clipFrame: CGRect diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 4043453c91b..c314403b0b4 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -15,6 +15,7 @@ import LegacyMessageInputPanel import LegacyMessageInputPanelInputView import AttachmentTextInputPanelNode import ChatSendMessageActionUI +import MinimizedContainer public enum AttachmentButtonType: Equatable { case gallery @@ -27,6 +28,29 @@ public enum AttachmentButtonType: Equatable { case gift case standalone + public var key: String { + switch self { + case .gallery: + return "gallery" + case .file: + return "file" + case .location: + return "location" + case .quickReply: + return "quickReply" + case .contact: + return "contact" + case .poll: + return "poll" + case let .app(bot): + return "app_\(bot.shortName)" + case .gift: + return "gift" + case .standalone: + return "standalone" + } + } + public static func ==(lhs: AttachmentButtonType, rhs: AttachmentButtonType) -> Bool { switch lhs { case .gallery: @@ -107,6 +131,8 @@ public protocol AttachmentContainable: ViewController { func requestDismiss(completion: @escaping () -> Void) func shouldDismissImmediately() -> Bool + + func beforeMaximize(navigationController: NavigationController, completion: @escaping () -> Void) } public extension AttachmentContainable { @@ -130,6 +156,10 @@ public extension AttachmentContainable { return true } + func beforeMaximize(navigationController: NavigationController, completion: @escaping () -> Void) { + completion() + } + var isPanGestureEnabled: (() -> Bool)? { return nil } @@ -206,10 +236,11 @@ public class AttachmentController: ViewController { private let updatedPresentationData: (initial: PresentationData, signal: Signal)? private let chatLocation: ChatLocation? private let isScheduledMessages: Bool - private let buttons: [AttachmentButtonType] + private var buttons: [AttachmentButtonType] private let initialButton: AttachmentButtonType private let fromMenu: Bool - private let hasTextInput: Bool + private var hasTextInput: Bool + private let isFullSize: Bool private let makeEntityInputView: () -> AttachmentTextInputPanelInputView? public var animateAppearance: Bool = false @@ -232,14 +263,14 @@ public class AttachmentController: ViewController { private final class Node: ASDisplayNode { private weak var controller: AttachmentController? - private let dim: ASDisplayNode + fileprivate let dim: ASDisplayNode private let shadowNode: ASImageNode fileprivate let container: AttachmentContainer private let makeEntityInputView: () -> AttachmentTextInputPanelInputView? let panel: AttachmentPanel - private var currentType: AttachmentButtonType? - private var currentControllers: [AttachmentContainable] = [] + fileprivate var currentType: AttachmentButtonType? + fileprivate var currentControllers: [AttachmentContainable] = [] private var validLayout: ContainerViewLayout? private var modalProgress: CGFloat = 0.0 @@ -305,6 +336,8 @@ public class AttachmentController: ViewController { private let wrapperNode: ASDisplayNode + private var isMinimizing = false + init(controller: AttachmentController, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) { self.controller = controller self.makeEntityInputView = makeEntityInputView @@ -319,7 +352,7 @@ public class AttachmentController: ViewController { self.wrapperNode = ASDisplayNode() self.wrapperNode.clipsToBounds = true - self.container = AttachmentContainer() + self.container = AttachmentContainer(isFullSize: controller.isFullSize) self.container.canHaveKeyboardFocus = true self.panel = AttachmentPanel(controller: controller, context: controller.context, chatLocation: controller.chatLocation, isScheduledMessages: controller.isScheduledMessages, updatedPresentationData: controller.updatedPresentationData, makeEntityInputView: makeEntityInputView) self.panel.fromMenu = controller.fromMenu @@ -327,6 +360,8 @@ public class AttachmentController: ViewController { super.init() + self.clipsToBounds = false + self.addSubnode(self.dim) self.addSubnode(self.shadowNode) self.addSubnode(self.wrapperNode) @@ -338,16 +373,21 @@ public class AttachmentController: ViewController { } } - self.container.updateModalProgress = { [weak self] progress, transition in + self.container.updateModalProgress = { [weak self] progress, topInset, bounds, transition in if let strongSelf = self, let layout = strongSelf.validLayout, !strongSelf.isDismissing { var transition = transition if strongSelf.container.supernode == nil { transition = .animated(duration: 0.4, curve: .spring) } - strongSelf.controller?.updateModalStyleOverlayTransitionFactor(progress, transition: transition) strongSelf.modalProgress = progress - strongSelf.containerLayoutUpdated(layout, transition: transition) + strongSelf.controller?.minimizedTopEdgeOffset = topInset + strongSelf.controller?.minimizedBounds = bounds + + if !strongSelf.isMinimizing { + strongSelf.controller?.updateModalStyleOverlayTransitionFactor(progress, transition: transition) + strongSelf.containerLayoutUpdated(layout, transition: transition) + } } } self.container.isReadyUpdated = { [weak self] in @@ -356,10 +396,24 @@ public class AttachmentController: ViewController { } } - self.container.interactivelyDismissed = { [weak self] in - if let strongSelf = self { - strongSelf.controller?.dismiss(animated: true) + self.container.interactivelyDismissed = { [weak self] velocity in + if let strongSelf = self, let layout = strongSelf.validLayout { + if let controller = strongSelf.controller, controller.shouldMinimizeOnSwipe?(strongSelf.currentType) == true { + var delta = layout.size.height + if let minimizedTopEdgeOffset = controller.minimizedTopEdgeOffset { + delta -= minimizedTopEdgeOffset + } + let damping: CGFloat = 180.0 + let initialVelocity: CGFloat = delta > 0.0 ? velocity / delta : 0.0 + + strongSelf.minimize(damping: damping, initialVelocity: initialVelocity) + + return false + } else { + strongSelf.controller?.dismiss(animated: true) + } } + return true } self.container.isPanningUpdated = { [weak self] value in @@ -523,6 +577,39 @@ public class AttachmentController: ViewController { } } + fileprivate func minimize(damping: CGFloat? = nil, initialVelocity: CGFloat? = nil) { + guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { + return + } + navigationController.minimizeViewController(controller, damping: damping, velocity: initialVelocity, beforeMaximize: { navigationController, completion in + if let controller = controller.mainController as? AttachmentContainable { + controller.beforeMaximize(navigationController: navigationController, completion: completion) + } else { + completion() + } + }, setupContainer: { [weak self] current in + let minimizedContainer: MinimizedContainerImpl? + if let current = current as? MinimizedContainerImpl { + minimizedContainer = current + } else if let context = self?.controller?.context { + minimizedContainer = MinimizedContainerImpl(sharedContext: context.sharedContext) + } else { + minimizedContainer = nil + } + return minimizedContainer + }, animated: true) + + self.dim.isHidden = true + + self.isMinimizing = true + self.container.update(isExpanded: true, force: true, transition: .immediate) + self.isMinimizing = false + + Queue.mainQueue().after(0.45, { + self.dim.isHidden = false + }) + } + fileprivate func updateSelectionCount(_ count: Int, animated: Bool = true) { self.selectionCount = count if let layout = self.validLayout { @@ -535,8 +622,12 @@ public class AttachmentController: ViewController { return } if case .ended = recognizer.state { - if let controller = self.currentControllers.last { - controller.requestDismiss(completion: { [weak self] in + if let lastController = self.currentControllers.last { + if let controller = self.controller, controller.shouldMinimizeOnSwipe?(self.currentType) == true { + self.minimize() + return + } + lastController.requestDismiss(completion: { [weak self] in self?.controller?.dismiss(animated: true) }) } else { @@ -798,7 +889,7 @@ public class AttachmentController: ViewController { return } - transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 2.0))) let fromMenu = controller.fromMenu @@ -871,7 +962,7 @@ public class AttachmentController: ViewController { self.wrapperNode.view.mask = nil } - + var containerInsets = containerLayout.intrinsicInsets var hasPanel = false let previousHasButton = self.hasButton @@ -889,7 +980,7 @@ public class AttachmentController: ViewController { if fromMenu && !hasButton, let inputContainerHeight = self.inputContainerHeight { panelHeight = inputContainerHeight } - if hasPanel || hasButton || (fromMenu && isCompact) { + if hasPanel || hasButton { containerInsets.bottom = panelHeight } @@ -982,7 +1073,9 @@ public class AttachmentController: ViewController { public var getSourceRect: (() -> CGRect?)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, chatLocation: ChatLocation?, isScheduledMessages: Bool = false, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false, hasTextInput: Bool = true, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil}) { + public var shouldMinimizeOnSwipe: ((AttachmentButtonType?) -> Bool)? + + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, chatLocation: ChatLocation?, isScheduledMessages: Bool = false, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false, hasTextInput: Bool = true, isFullSize: Bool = false, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil}) { self.context = context self.updatedPresentationData = updatedPresentationData self.chatLocation = chatLocation @@ -991,6 +1084,7 @@ public class AttachmentController: ViewController { self.initialButton = initialButton self.fromMenu = fromMenu self.hasTextInput = hasTextInput + self.isFullSize = isFullSize self.makeEntityInputView = makeEntityInputView super.init(navigationBarPresentationData: nil) @@ -1016,6 +1110,24 @@ public class AttachmentController: ViewController { return self.buttons.contains(.standalone) } + public func convertToStandalone() { + guard self.buttons != [.standalone] else { + return + } + if case let .app(bot) = self.node.currentType { + self.title = bot.peer.compactDisplayTitle + } + self.buttons = [.standalone] + self.hasTextInput = false + self.requestLayout(transition: .immediate) + } + + public func minimizeIfNeeded() { + if self.shouldMinimizeOnSwipe?(self.node.currentType) == true { + self.node.minimize() + } + } + public func updateSelectionCount(_ count: Int) { self.node.updateSelectionCount(count, animated: false) } @@ -1066,6 +1178,12 @@ public class AttachmentController: ViewController { return false } + public override var isMinimized: Bool { + didSet { + self.mainController.isMinimized = self.isMinimized + } + } + private var validLayout: ContainerViewLayout? override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -1081,6 +1199,10 @@ public class AttachmentController: ViewController { self.node.containerLayoutUpdated(layout, transition: transition) } + public var mainController: ViewController { + return self.node.currentControllers.first! + } + public final class InputPanelTransition { let inputNode: ASDisplayNode let accessoryPanelNode: ASDisplayNode? diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 927083c8220..92ab98ac7c2 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -79,7 +79,7 @@ private final class IconComponent: Component { self.disposable?.dispose() } - func update(component: IconComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: IconComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { if self.component?.name != component.name || self.component?.fileReference?.media.fileId != component.fileReference?.media.fileId || self.component?.tintColor != component.tintColor { if let fileReference = component.fileReference { let previousName = self.component?.name ?? "" @@ -117,7 +117,7 @@ private final class IconComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -701,7 +701,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { private let backgroundNode: NavigationBackgroundNode private let scrollNode: ASScrollNode private let separatorNode: ASDisplayNode - private var buttonViews: [Int: ComponentHostView] = [:] + private var buttonViews: [AnyHashable: ComponentHostView] = [:] private var textInputPanelNode: AttachmentTextInputPanelNode? private var progressNode: LoadingProgressNode? @@ -994,14 +994,16 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { let _ = (combineLatest( isReady, - captionIsAboveMedia |> take(1) + captionIsAboveMedia |> take(1), + ChatSendMessageContextScreen.initialData(context: strongSelf.context, currentMessageEffectId: nil) ) - |> deliverOnMainQueue).start(next: { [weak strongSelf] _, captionIsAboveMedia in + |> deliverOnMainQueue).start(next: { [weak strongSelf] _, captionIsAboveMedia, initialData in guard let strongSelf else { return } let controller = makeChatSendMessageActionSheetController( + initialData: initialData, context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, @@ -1014,6 +1016,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { } mediaPickerContext.setCaptionIsAboveMedia(value) }), + messageEffect: nil, attachment: true, canSendWhenOnline: sendWhenOnlineAvailable, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] @@ -1163,13 +1166,12 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { self.updateViews(transition: .init(animation: .curve(duration: 0.2, curve: .spring))) } - func updateViews(transition: Transition) { + func updateViews(transition: ComponentTransition) { guard let layout = self.validLayout else { return } let visibleRect = self.scrollNode.bounds.insetBy(dx: -180.0, dy: 0.0) - var validButtons = Set() var distanceBetweenNodes = layout.size.width / CGFloat(self.buttons.count) let internalWidth = distanceBetweenNodes * CGFloat(self.buttons.count - 1) @@ -1182,26 +1184,29 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { leftNodeOriginX = layout.safeInsets.left + sideInset + buttonWidth / 2.0 } + var validIds = Set() + for i in 0 ..< self.buttons.count { let originX = floor(leftNodeOriginX + CGFloat(i) * distanceBetweenNodes - buttonWidth / 2.0) let buttonFrame = CGRect(origin: CGPoint(x: originX, y: 0.0), size: CGSize(width: buttonWidth, height: buttonSize.height)) if !visibleRect.intersects(buttonFrame) { continue } - validButtons.insert(i) + + let type = self.buttons[i] + let _ = validIds.insert(type.key) var buttonTransition = transition let buttonView: ComponentHostView - if let current = self.buttonViews[i] { + if let current = self.buttonViews[type.key] { buttonView = current } else { buttonTransition = .immediate buttonView = ComponentHostView() - self.buttonViews[i] = buttonView + self.buttonViews[type.key] = buttonView self.scrollNode.view.addSubview(buttonView) } - let type = self.buttons[i] if case let .app(bot) = type { for (name, file) in bot.icons { if [.default, .iOSAnimated, .iOSSettingsStatic, .placeholder].contains(name) { @@ -1285,6 +1290,16 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { buttonView.accessibilityLabel = accessibilityTitle buttonView.accessibilityTraits = [.button] } + var removeIds: [AnyHashable] = [] + for (id, itemView) in self.buttonViews { + if !validIds.contains(id) { + removeIds.append(id) + itemView.removeFromSuperview() + } + } + for id in removeIds { + self.buttonViews.removeValue(forKey: id) + } } private func updateScrollLayoutIfNeeded(force: Bool, transition: ContainedViewLayoutTransition) -> Bool { diff --git a/submodules/AudioBlob/BUILD b/submodules/AudioBlob/BUILD index 425aef4797e..8d325911e3d 100644 --- a/submodules/AudioBlob/BUILD +++ b/submodules/AudioBlob/BUILD @@ -10,9 +10,11 @@ swift_library( #"-warnings-as-errors", ], deps = [ - "//submodules/AsyncDisplayKit:AsyncDisplayKit", - "//submodules/Display:Display", - "//submodules/LegacyComponents:LegacyComponents", + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/LegacyComponents", + "//submodules/MetalEngine", + "//submodules/TelegramUI/Components/Calls/CallScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index 37584408483..e6995380c35 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -34,7 +34,7 @@ private enum InnerState: Equatable { public final class AuthorizationSequenceController: NavigationController, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { static func navigationBarTheme(_ theme: PresentationTheme) -> NavigationBarTheme { - return NavigationBarTheme(buttonColor: theme.intro.accentTextColor, disabledButtonColor: theme.intro.disabledTextColor, primaryTextColor: theme.intro.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: theme.rootController.navigationBar.badgeBackgroundColor, badgeStrokeColor: theme.rootController.navigationBar.badgeStrokeColor, badgeTextColor: theme.rootController.navigationBar.badgeTextColor) + return NavigationBarTheme(buttonColor: theme.intro.accentTextColor, disabledButtonColor: theme.intro.disabledTextColor, primaryTextColor: theme.intro.primaryTextColor, backgroundColor: .clear, opaqueBackgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: theme.rootController.navigationBar.badgeBackgroundColor, badgeStrokeColor: theme.rootController.navigationBar.badgeStrokeColor, badgeTextColor: theme.rootController.navigationBar.badgeTextColor) } private let sharedContext: SharedAccountContext diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index aa92d85e404..7d7556f6889 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -1217,7 +1217,7 @@ public final class AvatarNode: ASDisplayNode { self.contentNode.setCustomLetters(letters, explicitColor: explicitColor, icon: icon) } - public func setStoryStats(storyStats: StoryStats?, presentationParams: StoryPresentationParams, transition: Transition) { + public func setStoryStats(storyStats: StoryStats?, presentationParams: StoryPresentationParams, transition: ComponentTransition) { if self.storyStats != storyStats || self.storyPresentationParams != presentationParams { self.storyStats = storyStats self.storyPresentationParams = presentationParams @@ -1267,7 +1267,7 @@ public final class AvatarNode: ASDisplayNode { } } - private func updateStoryIndicator(transition: Transition) { + private func updateStoryIndicator(transition: ComponentTransition) { if !self.isNodeLoaded { return } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift index 04b7ea9619c..988398d9991 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift @@ -17,7 +17,7 @@ public final class BotCheckoutController: ViewController { public let validatedFormInfo: BotPaymentValidatedFormInfo? public let botPeer: EnginePeer? - private init( + public init( form: BotPaymentForm, validatedFormInfo: BotPaymentValidatedFormInfo?, botPeer: EnginePeer? diff --git a/submodules/BrowserUI/BUILD b/submodules/BrowserUI/BUILD index 3c304bdd02a..0fcbb76fd6d 100644 --- a/submodules/BrowserUI/BUILD +++ b/submodules/BrowserUI/BUILD @@ -26,6 +26,7 @@ swift_library( "//submodules/Components/MultilineTextComponent:MultilineTextComponent", "//submodules/Components/BundleIconComponent:BundleIconComponent", "//submodules/Components/BlurredBackgroundComponent:BlurredBackgroundComponent", + "//submodules/TelegramUI/Components/MinimizedContainer", ], visibility = [ "//visibility:public", diff --git a/submodules/BrowserUI/Sources/BrowserContent.swift b/submodules/BrowserUI/Sources/BrowserContent.swift index 3609d871168..8f5f863582a 100644 --- a/submodules/BrowserUI/Sources/BrowserContent.swift +++ b/submodules/BrowserUI/Sources/BrowserContent.swift @@ -93,7 +93,7 @@ protocol BrowserContent: UIView { func scrollToTop() - func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: Transition) + func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) } struct ContentScrollingUpdate { @@ -102,7 +102,7 @@ struct ContentScrollingUpdate { public var absoluteOffsetToBottomEdge: CGFloat? public var isReset: Bool public var isInteracting: Bool - public var transition: Transition + public var transition: ComponentTransition public init( relativeOffset: CGFloat, @@ -110,7 +110,7 @@ struct ContentScrollingUpdate { absoluteOffsetToBottomEdge: CGFloat?, isReset: Bool, isInteracting: Bool, - transition: Transition + transition: ComponentTransition ) { self.relativeOffset = relativeOffset self.absoluteOffsetToTopEdge = absoluteOffsetToTopEdge diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index d3a781b7e48..e0004568688 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -873,7 +873,7 @@ //final class BrowserInstantPageContent: UIView, BrowserContent { // var onScrollingUpdate: (ContentScrollingUpdate) -> Void // -// func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentFlow.Transition) { +// func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentFlow.ComponentTransition) { // // } // diff --git a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift index 3476e0de77e..7e20575261d 100644 --- a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift @@ -289,7 +289,7 @@ private final class LoadingProgressComponent: Component { preconditionFailure() } - func update(component: LoadingProgressComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: LoadingProgressComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.lineView.backgroundColor = component.color let value = component.value @@ -306,14 +306,14 @@ private final class LoadingProgressComponent: Component { self.currentValue = value - let transition: Transition + let transition: ComponentTransition if animated && value > 0.0 { transition = .spring(duration: 0.7) } else { transition = .immediate } - let alphaTransition: Transition + let alphaTransition: ComponentTransition if animated { alphaTransition = .easeInOut(duration: 0.3) } else { @@ -333,7 +333,7 @@ private final class LoadingProgressComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -414,7 +414,7 @@ final class ReferenceButtonComponent: Component { self.component?.action() } - func update(component: ReferenceButtonComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: ReferenceButtonComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component let componentSize = self.componentView.update( @@ -441,7 +441,7 @@ final class ReferenceButtonComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 5108eaa2e47..c2f9f0dc4d9 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -13,6 +13,7 @@ import BundleIconComponent import TelegramUIPreferences import OpenInExternalAppUI import MultilineTextComponent +import MinimizedContainer private let settingsTag = GenericComponentViewTag() @@ -291,6 +292,7 @@ public class BrowserScreen: ViewController { guard let strongSelf = self else { return } + strongSelf.controller?.title = state.title strongSelf.contentState = state strongSelf.requestLayout(transition: .immediate) }).strict() @@ -318,7 +320,7 @@ public class BrowserScreen: ViewController { } self.controller?.present(shareController, in: .window(.root)) case .minimize: - break + self.minimize() case .openIn: self.context.sharedContext.applicationBindings.openUrl(url) case .openSettings: @@ -441,6 +443,25 @@ public class BrowserScreen: ViewController { self.requestLayout(transition: animated ? .easeInOut(duration: 0.2) : .immediate) } + func minimize() { + guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { + return + } + navigationController.minimizeViewController(controller, damping: nil, beforeMaximize: { _, completion in + completion() + }, setupContainer: { [weak self] current in + let minimizedContainer: MinimizedContainerImpl? + if let current = current as? MinimizedContainerImpl { + minimizedContainer = current + } else if let context = self?.controller?.context { + minimizedContainer = MinimizedContainerImpl(sharedContext: context.sharedContext) + } else { + minimizedContainer = nil + } + return minimizedContainer + }, animated: true) + } + func openSettings() { guard let referenceView = self.componentHost.findTaggedView(tag: settingsTag) as? ReferenceButtonComponent.View else { return @@ -535,7 +556,8 @@ public class BrowserScreen: ViewController { self.context.sharedContext.applicationBindings.openUrl(openInUrl) } action(.default) - }))] + })) + ] let contextController = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items)))) self.controller?.present(contextController, in: .window(.root)) @@ -600,13 +622,13 @@ public class BrowserScreen: ViewController { } } - func requestLayout(transition: Transition) { + func requestLayout(transition: ComponentTransition) { if let (layout, navigationBarHeight) = self.validLayout { self.containerLayoutUpdated(layout: layout, navigationBarHeight: navigationBarHeight, transition: transition) } } - func containerLayoutUpdated(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: Transition) { + func containerLayoutUpdated(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ComponentTransition) { self.validLayout = (layout, navigationBarHeight) let environment = ViewControllerComponentContainer.Environment( @@ -705,7 +727,7 @@ public class BrowserScreen: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - (self.displayNode as! Node).containerLayoutUpdated(layout: layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.height, transition: Transition(transition)) + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.height, transition: ComponentTransition(transition)) } } diff --git a/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift b/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift index df776d64d2c..c49fa13540a 100644 --- a/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift @@ -240,7 +240,7 @@ final class SearchBarContentComponent: Component { } } - func update(component: SearchBarContentComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: SearchBarContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component self.update(theme: component.theme, strings: component.strings, size: availableSize, transition: transition) @@ -249,7 +249,7 @@ final class SearchBarContentComponent: Component { return availableSize } - public func update(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: Transition) { + public func update(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: ComponentTransition) { let params = Params( theme: theme, strings: strings, @@ -351,7 +351,7 @@ final class SearchBarContentComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/BrowserUI/Sources/BrowserStackContainerNode.swift b/submodules/BrowserUI/Sources/BrowserStackContainerNode.swift deleted file mode 100644 index 17b244e3311..00000000000 --- a/submodules/BrowserUI/Sources/BrowserStackContainerNode.swift +++ /dev/null @@ -1,445 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import AppBundle - -let maxInteritemSpacing: CGFloat = 240.0 -let sectionInsetTop: CGFloat = 40.0 -let sectionInsetBottom: CGFloat = 0.0 -let zOffset: CGFloat = -60.0 - -let perspectiveCorrection: CGFloat = -1.0 / 1000.0 -let maxRotationAngle: CGFloat = -CGFloat.pi / 2.2 - -extension CATransform3D { - func interpolate(other: CATransform3D, progress: CGFloat) -> CATransform3D { - var vectors = Array(repeating: 0.0, count: 16) - vectors[0] = self.m11 + (other.m11 - self.m11) * progress - vectors[1] = self.m12 + (other.m12 - self.m12) * progress - vectors[2] = self.m13 + (other.m13 - self.m13) * progress - vectors[3] = self.m14 + (other.m14 - self.m14) * progress - vectors[4] = self.m21 + (other.m21 - self.m21) * progress - vectors[5] = self.m22 + (other.m22 - self.m22) * progress - vectors[6] = self.m23 + (other.m23 - self.m23) * progress - vectors[7] = self.m24 + (other.m24 - self.m24) * progress - vectors[8] = self.m31 + (other.m31 - self.m31) * progress - vectors[9] = self.m32 + (other.m32 - self.m32) * progress - vectors[10] = self.m33 + (other.m33 - self.m33) * progress - vectors[11] = self.m34 + (other.m34 - self.m34) * progress - vectors[12] = self.m41 + (other.m41 - self.m41) * progress - vectors[13] = self.m42 + (other.m42 - self.m42) * progress - vectors[14] = self.m43 + (other.m43 - self.m43) * progress - vectors[15] = self.m44 + (other.m44 - self.m44) * progress - - return CATransform3D(m11: vectors[0], m12: vectors[1], m13: vectors[2], m14: vectors[3], m21: vectors[4], m22: vectors[5], m23: vectors[6], m24: vectors[7], m31: vectors[8], m32: vectors[9], m33: vectors[10], m34: vectors[11], m41: vectors[12], m42: vectors[13], m43: vectors[14], m44: vectors[15]) - } -} - - -private func angle(for origin: CGFloat, itemCount: Int, bounds: CGRect, contentHeight: CGFloat?) -> CGFloat { - var rotationAngle = rotationAngleAt0(itemCount: itemCount) - - var contentOffset = bounds.origin.y - if contentOffset < 0.0 { - contentOffset *= 2.0 - } -// } else if let contentHeight = contentHeight, bounds.maxY > contentHeight { -//// let maxContentOffset = contentHeight - bounds.height -//// let delta = contentOffset - maxContentOffset -//// contentOffset = maxContentOffset + delta / 2.0 -// } - - var yOnScreen = origin - contentOffset - sectionInsetTop - if yOnScreen < 0 { - yOnScreen = 0 - } else if yOnScreen > bounds.height { - yOnScreen = bounds.height - } - - let maxRotationVariance = maxRotationAngle - rotationAngleAt0(itemCount: itemCount) - rotationAngle += (maxRotationVariance / bounds.height) * yOnScreen - - return rotationAngle -} - -private func final3dTransform(for origin: CGFloat, size: CGSize, contentHeight: CGFloat?, itemCount: Int, forcedAngle: CGFloat? = nil, additionalAngle: CGFloat? = nil, bounds: CGRect) -> CATransform3D { - var transform = CATransform3DIdentity - transform.m34 = perspectiveCorrection - - let rotationAngle = forcedAngle ?? angle(for: origin, itemCount: itemCount, bounds: bounds, contentHeight: contentHeight) - var effectiveRotationAngle = rotationAngle - if let additionalAngle = additionalAngle { - effectiveRotationAngle += additionalAngle - } - - let r = size.height / 2.0 + abs(zOffset / sin(rotationAngle)) - - let zTranslation = r * sin(rotationAngle) - let yTranslation: CGFloat = r * (1 - cos(rotationAngle)) - - let zTranslateTransform = CATransform3DTranslate(transform, 0.0, -yTranslation, zTranslation) - - let rotateTransform = CATransform3DRotate(zTranslateTransform, effectiveRotationAngle, 1.0, 0.0, 0.0) - - return rotateTransform -} - -private func interitemSpacing(itemCount: Int, bounds: CGRect) -> CGFloat { - var interitemSpacing = maxInteritemSpacing - if itemCount > 0 { - interitemSpacing = (bounds.height - sectionInsetTop - sectionInsetBottom) / CGFloat(min(itemCount, 5)) - } - return interitemSpacing -} - -private func frameForIndex(index: Int, size: CGSize, itemCount: Int, bounds: CGRect) -> CGRect { - let spacing = interitemSpacing(itemCount: itemCount, bounds: bounds) - let y = sectionInsetTop + spacing * CGFloat(index) - let origin = CGPoint(x: 0, y: y) - - return CGRect(origin: origin, size: size) -} - -private func rotationAngleAt0(itemCount: Int) -> CGFloat { - let multiplier: CGFloat = min(CGFloat(itemCount), 5.0) - 1.0 - return -CGFloat.pi / 7.0 - CGFloat.pi / 7.0 * multiplier / 4.0 -} - -private let shadowImage: UIImage? = { - return generateImage(CGSize(width: 1.0, height: 640.0), rotatedContext: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - - let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor] as CFArray - - var locations: [CGFloat] = [0.0, 0.65, 1.0] - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! - context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: bounds.height), options: []) - }) -}() - -class StackItemContainerNode: ASDisplayNode { - private let node: ASDisplayNode - private let shadowNode: ASImageNode - - var tapped: (() -> Void)? - var highlighted: ((Bool) -> Void)? - - init(node: ASDisplayNode) { - self.node = node - self.shadowNode = ASImageNode() - self.shadowNode.displaysAsynchronously = false - self.shadowNode.displayWithoutProcessing = true - self.shadowNode.contentMode = .scaleToFill - - super.init() - - self.clipsToBounds = true - self.cornerRadius = 10.0 - applySmoothRoundedCorners(self.layer) - - self.shadowNode.image = shadowImage - - self.addSubnode(self.node) - self.addSubnode(self.shadowNode) - } - - override func didLoad() { - super.didLoad() - - let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) - recognizer.tapActionAtPoint = { point in - return .waitForSingleTap - } - recognizer.highlight = { [weak self] point in - if let point = point, point.x > 280.0 { - self?.highlighted?(true) - } else { - self?.highlighted?(false) - } - } - self.view.addGestureRecognizer(recognizer) - } - - func animateIn() { - self.shadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - } - - @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { - switch recognizer.state { - case .ended: - if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { - switch gesture { - case .tap: - self.tapped?() - default: - break - } - } - default: - break - } - } - - override func layout() { - super.layout() - - self.node.frame = self.bounds - self.shadowNode.frame = self.bounds - } -} - -public class StackContainerNode: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDelegate { - private let scrollNode: ASScrollNode - private var nodes: [StackItemContainerNode] - - private var deleteGestureRecognizer: UIPanGestureRecognizer? - private var offsetsForDeletingItems: [Int: CGPoint]? - private var currentDeletingIndexPath: Int? - private var deletingOffset: CGFloat? - - private var animatingIn = false - - private var validLayout: CGSize? - - override public init() { - self.scrollNode = ASScrollNode() - self.nodes = [] - - super.init() - - self.backgroundColor = .black - - self.addSubnode(self.scrollNode) - } - - override public func didLoad() { - super.didLoad() - - if #available(iOS 11.0, *) { - self.scrollNode.view.contentInsetAdjustmentBehavior = .never - } - - self.scrollNode.view.delegate = self.wrappedScrollViewDelegate - self.scrollNode.view.alwaysBounceVertical = true - - let deleteGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didPanToDelete(gestureRecognizer:))) - deleteGestureRecognizer.delegate = self.wrappedGestureRecognizerDelegate - deleteGestureRecognizer.delaysTouchesBegan = true - self.scrollNode.view.addGestureRecognizer(deleteGestureRecognizer) - self.deleteGestureRecognizer = deleteGestureRecognizer - } - - func item(forYPosition y: CGFloat) -> Int? { - let itemCount = self.nodes.count - let bounds = self.scrollNode.bounds - - let spacing = interitemSpacing(itemCount: itemCount, bounds: bounds) - return max(0, min(Int(floor((y - sectionInsetTop) / spacing)), itemCount - 1)) - } - - public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else { - return false - } - - let touch = panGesture.location(in: gestureRecognizer.view) - let velocity = panGesture.velocity(in: gestureRecognizer.view) - - if abs(velocity.x) > abs(velocity.y), let item = self.item(forYPosition: touch.y) { - return item > 0 - } - return false - } - - @objc func didPanToDelete(gestureRecognizer: UIPanGestureRecognizer) { - let scrollView = self.scrollNode.view - - switch gestureRecognizer.state { - case .began: - let touch = gestureRecognizer.location(in: scrollView) - guard let item = self.item(forYPosition: touch.y) else { return } - - self.currentDeletingIndexPath = item - case .changed: - guard let _ = self.currentDeletingIndexPath else { return } - - var delta = gestureRecognizer.translation(in: scrollView) - delta.y = 0 - - if let offset = self.deletingOffset { - self.deletingOffset = offset + delta.x - } else { - self.deletingOffset = delta.x - } - - gestureRecognizer.setTranslation(.zero, in: scrollView) - - self.updateLayout() - case .ended: - if let _ = self.currentDeletingIndexPath { - if let offset = self.deletingOffset { - if offset < -self.frame.width / 2.0 { - self.deletingOffset = -self.frame.width - } else { - self.deletingOffset = nil - self.currentDeletingIndexPath = nil - } - } - } - - UIView.animate(withDuration: 0.3) { - self.updateLayout() - } - case .cancelled, .failed: - self.currentDeletingIndexPath = nil - self.deletingOffset = nil - default: - break - } - } - - func setup() { - let images: [UIImage] = [UIImage(bundleImageName: "Settings/test1")!, UIImage(bundleImageName: "Settings/test5")!, UIImage(bundleImageName: "Settings/test4")!, UIImage(bundleImageName: "Settings/test3")!, UIImage(bundleImageName: "Settings/test2")!] - for i in 0 ..< 5 { - let node = ASImageNode() - node.image = images[i] - - let containerNode = StackItemContainerNode(node: node) - containerNode.tapped = { [weak self] in - self?.animateIn(index: i) - } - containerNode.highlighted = { [weak self] highlighted in - self?.highlight(index: i, value: highlighted) - } - self.nodes.append(containerNode) - } - - var index: Int = 0 - let bounds = self.scrollNode.view.bounds - let itemCount = self.nodes.count - - for node in self.nodes { - self.scrollNode.addSubnode(node) - - let size = CGSize(width: self.frame.width, height: self.frame.height) - let frame = frameForIndex(index: index, size: size, itemCount: itemCount, bounds: bounds) - node.frame = frame - let transform = final3dTransform(for: frame.minY, size: frame.size, contentHeight: nil, itemCount: itemCount, bounds: bounds) - node.transform = transform - index += 1 - } - - if let lastFrame = self.nodes.last?.frame { - self.scrollNode.view.contentSize = CGSize(width: self.frame.width, height: lastFrame.minY) - } - } - - public func animateIn(index: Int) { - let node = self.nodes[index] - - self.animatingIn = true - self.scrollNode.view.isUserInteractionEnabled = false - node.animateIn() - UIView.animate(withDuration: 0.3) { - node.transform = CATransform3DIdentity - node.position = CGPoint(x: self.scrollNode.frame.width / 2.0, y: self.scrollNode.frame.height / 2.0) - } - - for i in 0 ..< index { - let node = self.nodes[i] - node.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -550.0), duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, mediaTimingFunction: nil, removeOnCompletion: false, additive: true, force: false, completion: nil) - } - - for i in (index + 1) ..< self.nodes.count { - let node = self.nodes[i] - node.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 550.0), duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, mediaTimingFunction: nil, removeOnCompletion: false, additive: true, force: false, completion: nil) - } - } - - public func highlight(index: Int, value: Bool) { - let node = self.nodes[index] - - let bounds = self.scrollNode.view.bounds - let contentHeight = self.scrollNode.view.contentSize.height - let itemCount = self.nodes.count - - UIView.animate(withDuration: 0.4) { - let transform = final3dTransform(for: node.frame.minY, size: node.frame.size, contentHeight: contentHeight, itemCount: itemCount, additionalAngle: value ? 0.04 : nil, bounds: bounds) - node.transform = transform - } - } - - public func scrollViewDidScroll(_ scrollView: UIScrollView) { - guard !self.animatingIn else { - return - } - self.updateLayout() - } - - func updateLayout() { - let bounds = self.scrollNode.view.bounds - let contentHeight = self.scrollNode.view.contentSize.height - let itemCount = self.nodes.count - - var index: Int = 0 - for node in self.nodes { - let initialTransform = final3dTransform(for: node.frame.minY, size: node.frame.size, contentHeight: contentHeight, itemCount: itemCount, bounds: bounds) - let initialFrame = frameForIndex(index: index, size: node.frame.size, itemCount: itemCount, bounds: bounds) - - var targetTransform: CATransform3D? - var targetPosition: CGPoint? - - var finalPosition = initialFrame.center - - if let deletingIndex = self.currentDeletingIndexPath, let offset = self.deletingOffset { - if deletingIndex == index { - finalPosition = CGPoint(x: self.frame.width / 2.0 + min(offset, 0.0), y: node.position.y) - } else if index < deletingIndex { - let frame = frameForIndex(index: index, size: node.frame.size, itemCount: itemCount - 1, bounds: bounds) - targetPosition = frame.center - - let spacing = interitemSpacing(itemCount: itemCount - 1, bounds: bounds) - targetTransform = final3dTransform(for: frame.minY, size: node.frame.size, contentHeight: contentHeight - node.frame.height - spacing, itemCount: itemCount - 1, bounds: bounds) - } else { - let frame = frameForIndex(index: index - 1, size: node.frame.size, itemCount: itemCount - 1, bounds: bounds) - targetPosition = frame.center - - let spacing = interitemSpacing(itemCount: itemCount - 1, bounds: bounds) - targetTransform = final3dTransform(for: frame.minY, size: node.frame.size, contentHeight: contentHeight - node.frame.height - spacing, itemCount: itemCount - 1, bounds: bounds) - } - } else { - node.position = initialFrame.center - } - - var finalTransform = initialTransform - if let targetTransform = targetTransform, let offset = self.deletingOffset { - let progress = min(1.0, abs(offset / (self.frame.width))) - finalTransform = initialTransform.interpolate(other: targetTransform, progress: progress) - } - - if let targetPosition = targetPosition, let offset = self.deletingOffset { - let progress = min(1.0, abs(offset / (self.frame.width))) - finalPosition = CGPoint(x: finalPosition.x + (targetPosition.x - finalPosition.x) * progress, y: finalPosition.y + (targetPosition.y - finalPosition.y) * progress) - } - - node.transform = finalTransform - node.position = finalPosition - - index += 1 - } - } - - public func update(size: CGSize) { - let hadValidLayout = self.validLayout != nil - self.validLayout = size - - self.scrollNode.frame = CGRect(origin: CGPoint(), size: size) - - if !hadValidLayout { - self.setup() - } - } -} diff --git a/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift b/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift index 00a8c2c0390..ad57dc233e3 100644 --- a/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift @@ -100,11 +100,13 @@ final class BrowserToolbarComponent: CombinedComponent { if let centerItem = item { context.add(centerItem .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight / 2.0 + offset)) - .appear(Transition.Appear({ _, view, transition in + .appear(ComponentTransition.Appear({ _, view, transition in transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: size.height), to: .zero, additive: true) })) - .disappear(Transition.Disappear({ view, transition, completion in - transition.animatePosition(view: view, from: .zero, to: CGPoint(x: 0.0, y: size.height), additive: true, completion: { _ in + .disappear(ComponentTransition.Disappear({ view, transition, completion in + let from = view.center + view.center = from.offsetBy(dx: 0.0, dy: size.height) + transition.animatePosition(view: view, from: from, to: view.center, completion: { _ in completion() }) })) @@ -224,12 +226,12 @@ final class NavigationToolbarContentComponent: CombinedComponent { component: Button( content: AnyComponent( BundleIconComponent( - name: "Chat/Context Menu/Browser", + name: "Instant View/Minimize", tintColor: context.component.textColor ) ), action: { - performAction.invoke(.openIn) + performAction.invoke(.minimize) } ).minSize(buttonSize), availableSize: buttonSize, diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 06b57d7a719..9f227afc8be 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -258,7 +258,7 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { self.webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.webView.scrollView.contentInset.top), animated: true) } - func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: Transition) { + func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) { var scrollInsets = insets scrollInsets.top = 0.0 if self.webView.scrollView.contentInset != insets { @@ -303,7 +303,7 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { } private func snapScrollingOffsetToInsets() { - let transition = Transition(animation: .curve(duration: 0.4, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)) self.updateScrollingOffset(isReset: false, transition: transition) } @@ -317,7 +317,7 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { self.snapScrollingOffsetToInsets() } - private func updateScrollingOffset(isReset: Bool, transition: Transition) { + private func updateScrollingOffset(isReset: Bool, transition: ComponentTransition) { let scrollView = self.webView.scrollView let isInteracting = scrollView.isDragging || scrollView.isDecelerating if let previousScrollingOffsetValue = self.previousScrollingOffset { diff --git a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift index 078503777c4..991e2bea6db 100644 --- a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift +++ b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift @@ -404,7 +404,7 @@ private final class DayComponent: Component { self.action?() } - func update(component: DayComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(component: DayComponent, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { let isFirstTime = self.action == nil self.action = component.action @@ -613,7 +613,7 @@ private final class DayComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } @@ -859,7 +859,7 @@ private final class MonthComponent: CombinedComponent { let delayIndex = dayEnvironment.selectionDelayCoordination context.add(selection .position(CGPoint(x: selectionRect.midX, y: selectionRect.midY)) - .appear(Transition.Appear { _, view, transition in + .appear(ComponentTransition.Appear { _, view, transition in if case .none = transition.animation { return } @@ -867,7 +867,7 @@ private final class MonthComponent: CombinedComponent { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.05, delay: delay) view.layer.animateFrame(from: CGRect(origin: view.frame.origin, size: CGSize(width: selectionRadius, height: view.frame.height)), to: view.frame, duration: 0.25, delay: delay, timingFunction: kCAMediaTimingFunctionSpring) }) - .disappear(Transition.Disappear { view, transition, completion in + .disappear(ComponentTransition.Disappear { view, transition, completion in if case .none = transition.animation { completion() return @@ -1203,12 +1203,12 @@ public final class CalendarMessageScreen: ViewController { } func toggleSelectionMode() { - var transition: Transition = .immediate + var transition: ComponentTransition = .immediate if self.selectionState == nil { self.selectionState = SelectionState(dayRange: nil) } else { self.selectionState = nil - transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)) transition = transition.withUserData(SelectionTransition.end) } @@ -1236,7 +1236,7 @@ public final class CalendarMessageScreen: ViewController { self.selectionToolbarActionSelected() } - func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition, componentsTransition: Transition) { + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition, componentsTransition: ComponentTransition) { let isFirstLayout = self.validLayout == nil self.validLayout = (layout, navigationHeight) @@ -1614,7 +1614,7 @@ public final class CalendarMessageScreen: ViewController { return true } - func updateMonthViews(transition: Transition) { + func updateMonthViews(transition: ComponentTransition) { guard let (width, _, frames) = self.scrollLayout else { return } @@ -1657,7 +1657,7 @@ public final class CalendarMessageScreen: ViewController { return } if var selectionState = strongSelf.selectionState { - var transition = Transition(animation: .curve(duration: 0.2, curve: .spring)) + var transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .spring)) if let dayRange = selectionState.dayRange { if dayRange.lowerBound == timestamp || dayRange.upperBound == timestamp { selectionState.dayRange = nil @@ -1712,7 +1712,7 @@ public final class CalendarMessageScreen: ViewController { guard var selectionState = strongSelf.selectionState else { return } - var transition = Transition(animation: .curve(duration: 0.2, curve: .spring)) + var transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .spring)) if let dayRange = selectionState.dayRange { if dayRange == range { selectionState.dayRange = nil @@ -1751,7 +1751,7 @@ public final class CalendarMessageScreen: ViewController { } } - private func updateSelectionState(transition: Transition) { + private func updateSelectionState(transition: ComponentTransition) { var title = self.presentationData.strings.MessageCalendar_Title if let selectionState = self.selectionState, let dayRange = selectionState.dayRange { var selectedCount = 0 diff --git a/submodules/Camera/Sources/CameraMetrics.swift b/submodules/Camera/Sources/CameraMetrics.swift index 8bef2e9d077..c1c8a3e4296 100644 --- a/submodules/Camera/Sources/CameraMetrics.swift +++ b/submodules/Camera/Sources/CameraMetrics.swift @@ -46,8 +46,10 @@ public extension Camera { return [1.0] case .iPhone14, .iPhone14Plus, .iPhone15, .iPhone15Plus: return [0.5, 1.0, 2.0] - case .iPhone14Pro, .iPhone14ProMax, .iPhone15Pro, .iPhone15ProMax: + case .iPhone14Pro, .iPhone14ProMax, .iPhone15Pro: return [0.5, 1.0, 2.0, 3.0] + case .iPhone15ProMax: + return [0.5, 1.0, 2.0, 5.0] case .unknown: return [1.0, 2.0] } diff --git a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift index 7fd19cb3b72..f2757ae74c3 100644 --- a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift +++ b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift @@ -474,13 +474,14 @@ public final class ChatInterfaceState: Codable, Equatable { public let forwardAsCopy: Bool // public let inputLanguage: String? + public let sendMessageEffect: Int64? public var synchronizeableInputState: SynchronizeableChatInputState? { if self.composeInputState.inputText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && self.replyMessageSubject == nil { return nil } else { let sourceText = expandedInputStateAttributedString(self.composeInputState.inputText) - return SynchronizeableChatInputState(replySubject: self.replyMessageSubject?.subjectModel, text: sourceText.string, entities: generateChatInputTextEntities(sourceText), timestamp: self.timestamp, textSelection: self.composeInputState.selectionRange) + return SynchronizeableChatInputState(replySubject: self.replyMessageSubject?.subjectModel, text: sourceText.string, entities: generateChatInputTextEntities(sourceText), timestamp: self.timestamp, textSelection: self.composeInputState.selectionRange, messageEffectId: self.sendMessageEffect) } } @@ -527,10 +528,11 @@ public final class ChatInterfaceState: Codable, Equatable { self.forwardAsCopy = false // self.inputLanguage = nil + self.sendMessageEffect = nil } // MARK: Nicegram (forwardAsCopy) - public init(timestamp: Int32, composeInputState: ChatTextInputState, composeDisableUrlPreviews: [String], replyMessageSubject: ReplyMessageSubject?, forwardMessageIds: [EngineMessage.Id]?, forwardOptionsState: ChatInterfaceForwardOptionsState?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState, historyScrollState: ChatInterfaceHistoryScrollState?, mediaRecordingMode: ChatTextInputMediaRecordingButtonMode, mediaDraftState: ChatInterfaceMediaDraftState?, silentPosting: Bool, forwardAsCopy: Bool = false, inputLanguage: String?) { + public init(timestamp: Int32, composeInputState: ChatTextInputState, composeDisableUrlPreviews: [String], replyMessageSubject: ReplyMessageSubject?, forwardMessageIds: [EngineMessage.Id]?, forwardOptionsState: ChatInterfaceForwardOptionsState?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState, historyScrollState: ChatInterfaceHistoryScrollState?, mediaRecordingMode: ChatTextInputMediaRecordingButtonMode, mediaDraftState: ChatInterfaceMediaDraftState?, silentPosting: Bool, forwardAsCopy: Bool = false, inputLanguage: String?, sendMessageEffect: Int64?) { self.timestamp = timestamp self.composeInputState = composeInputState self.composeDisableUrlPreviews = composeDisableUrlPreviews @@ -548,6 +550,7 @@ public final class ChatInterfaceState: Codable, Equatable { self.forwardAsCopy = forwardAsCopy // self.inputLanguage = inputLanguage + self.sendMessageEffect = sendMessageEffect } public init(from decoder: Decoder) throws { @@ -622,6 +625,8 @@ public final class ChatInterfaceState: Codable, Equatable { self.forwardAsCopy = ((try? container.decode(Int32.self, forKey: "fwdcpy")) ?? 0) != 0 // self.inputLanguage = try? container.decodeIfPresent(String.self, forKey: "inputLanguage") + + self.sendMessageEffect = try? container.decodeIfPresent(Int64.self, forKey: "sendMessageEffect") } public func encode(to encoder: Encoder) throws { @@ -681,6 +686,8 @@ public final class ChatInterfaceState: Codable, Equatable { } else { try container.encodeNil(forKey: "inputLanguage") } + + try container.encodeIfPresent(self.sendMessageEffect, forKey: "sendMessageEffect") } public static func ==(lhs: ChatInterfaceState, rhs: ChatInterfaceState) -> Bool { @@ -720,23 +727,26 @@ public final class ChatInterfaceState: Codable, Equatable { if lhs.inputLanguage != rhs.inputLanguage { return false } + if lhs.sendMessageEffect != rhs.sendMessageEffect { + return false + } return lhs.composeInputState == rhs.composeInputState && lhs.replyMessageSubject == rhs.replyMessageSubject && lhs.selectionState == rhs.selectionState && lhs.editMessage == rhs.editMessage } // MARK: Nicegram ForwardAsCopy public func withUpdatedForwardAsCopy(_ forwardAsCopy: Bool) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: self.sendMessageEffect) } // public func withUpdatedComposeInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { let updatedComposeInputState = inputState - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: self.sendMessageEffect) } public func withUpdatedComposeDisableUrlPreviews(_ disableUrlPreviews: [String]) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: disableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: disableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: self.sendMessageEffect) } public func withUpdatedEffectiveInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { @@ -748,19 +758,19 @@ public final class ChatInterfaceState: Codable, Equatable { updatedComposeInputState = inputState } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: self.sendMessageEffect) } public func withUpdatedReplyMessageSubject(_ replyMessageSubject: ReplyMessageSubject?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: self.sendMessageEffect) } public func withUpdatedForwardMessageIds(_ forwardMessageIds: [EngineMessage.Id]?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: self.sendMessageEffect) } public func withUpdatedForwardOptionsState(_ forwardOptionsState: ChatInterfaceForwardOptionsState?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: self.sendMessageEffect) } public func withUpdatedSelectedMessages(_ messageIds: [EngineMessage.Id]) -> ChatInterfaceState { @@ -771,7 +781,7 @@ public final class ChatInterfaceState: Codable, Equatable { for messageId in messageIds { selectedIds.insert(messageId) } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: self.sendMessageEffect) } public func withToggledSelectedMessages(_ messageIds: [EngineMessage.Id], value: Bool) -> ChatInterfaceState { @@ -786,43 +796,47 @@ public final class ChatInterfaceState: Codable, Equatable { selectedIds.remove(messageId) } } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: self.sendMessageEffect) } public func withoutSelectionState() -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: self.sendMessageEffect) } public func withUpdatedTimestamp(_ timestamp: Int32) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: self.sendMessageEffect) } public func withUpdatedEditMessage(_ editMessage: ChatEditMessageState?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: self.sendMessageEffect) } public func withUpdatedMessageActionsState(_ f: (ChatInterfaceMessageActionsState) -> ChatInterfaceMessageActionsState) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState), historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState), historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: self.sendMessageEffect) } public func withUpdatedHistoryScrollState(_ historyScrollState: ChatInterfaceHistoryScrollState?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: self.sendMessageEffect) } public func withUpdatedMediaRecordingMode(_ mediaRecordingMode: ChatTextInputMediaRecordingButtonMode) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: self.sendMessageEffect) } public func withUpdatedMediaDraftState(_ mediaDraftState: ChatInterfaceMediaDraftState?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: self.sendMessageEffect) } public func withUpdatedSilentPosting(_ silentPosting: Bool) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: self.sendMessageEffect) } public func withUpdatedInputLanguage(_ inputLanguage: String?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: inputLanguage, sendMessageEffect: self.sendMessageEffect) + } + + public func withUpdatedSendMessageEffect(_ sendMessageEffect: Int64?) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, mediaDraftState: self.mediaDraftState, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage, sendMessageEffect: sendMessageEffect) } // diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index eeede04cfe6..5935c17f1f4 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -121,6 +121,7 @@ swift_library( "//submodules/TelegramUI/Components/Settings/BirthdayPickerScreen", "//submodules/Components/MultilineTextComponent", "//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen", + "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift b/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift index 4d9d6eadaca..c28d06df04e 100644 --- a/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift +++ b/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift @@ -303,7 +303,7 @@ final class ChatListContainerItemNode: ASDisplayNode { if let chatFolderUpdates = self.chatFolderUpdates { let topPanel: TopPanelItem - var topPanelTransition = Transition(transition) + var topPanelTransition = ComponentTransition(transition) if let current = self.topPanel { topPanel = current } else { @@ -350,7 +350,7 @@ final class ChatListContainerItemNode: ASDisplayNode { additionalTopInset += topPanelHeight } else if self.canReportPeer { let topPanel: TopPanelItem - var topPanelTransition = Transition(transition) + var topPanelTransition = ComponentTransition(transition) if let current = self.topPanel { topPanel = current } else { diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index e26e89ac858..8c2079cfdfe 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -68,6 +68,7 @@ import FullScreenEffectView import PeerInfoStoryGridScreen import ArchiveInfoScreen import BirthdayPickerScreen +import OldChannelsController private final class ContextControllerContentSourceImpl: ContextControllerContentSource { let controller: ViewController @@ -953,7 +954,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - self.chatListDisplayNode.requestNavigationBarLayout(transition: Transition.immediate.withUserData(ChatListNavigationBar.AnimationHint( + self.chatListDisplayNode.requestNavigationBarLayout(transition: ComponentTransition.immediate.withUserData(ChatListNavigationBar.AnimationHint( disableStoriesAnimations: false, crossfadeStoryPeers: true ))) @@ -1427,7 +1428,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let source: ContextContentSource let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .replyThread(message: ChatReplyThreadMessage( peerId: peer.peerId, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false - )), subject: nil, botStart: nil, mode: .standard(.previewing)) + )), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) @@ -1456,7 +1457,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let location = location { source = .location(ChatListContextLocationContentSource(controller: strongSelf, location: location)) } else { - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.peerId), subject: nil, botStart: nil, mode: .standard(.previewing)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.peerId), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) } @@ -1475,7 +1476,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let source: ContextContentSource let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .replyThread(message: ChatReplyThreadMessage( peerId: peer.peerId, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false - )), subject: nil, botStart: nil, mode: .standard(.previewing)) + )), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) @@ -1525,7 +1526,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if case let .search(messageId) = source, let id = messageId { subject = .message(id: .id(id), highlight: nil, timecode: nil) } - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.id), subject: subject, botStart: nil, mode: .standard(.previewing)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.id), subject: subject, botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) contextContentSource = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) } @@ -3012,6 +3013,20 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) }))) } else if case let .channel(channel) = peer { + if channel.hasPermission(.postStories) { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryFeed_ContextAddStory, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c?.dismiss(completion: { + guard let self else { + return + } + + self.openStoryCamera(fromList: true) + }) + }))) + } + let openTitle: String let openIcon: String switch channel.info { @@ -3581,7 +3596,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default)) + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) if let sourceController = sourceController as? ChatListControllerImpl, case .forum(peerId) = sourceController.location { navigationController.replaceController(sourceController, with: chatController, animated: false) @@ -4891,89 +4906,186 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController ]) self.present(actionSheet, in: .window(.root)) } else if !peerIds.isEmpty { - let actionSheet = ActionSheetController(presentationData: self.presentationData) - var items: [ActionSheetItem] = [] - items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteConfirmation(Int32(peerIds.count)), color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - - guard let strongSelf = self else { + let _ = (self.context.engine.data.get( + EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ) + |> deliverOnMainQueue).start(next: { [weak self] peers in + guard let self else { return } - strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in - var state = state - for peerId in peerIds { - state.pendingRemovalItemIds.insert(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) + var havePrivateChats = false + var haveNonPrivateChats = false + for peer in peers { + if let peer { + switch peer { + case .user, .secretChat: + havePrivateChats = true + default: + haveNonPrivateChats = true + } } - return state - }) - - let text = strongSelf.presentationData.strings.ChatList_DeletedChats(Int32(peerIds.count)) + } - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in - guard let strongSelf = self else { - return false - } - if value == .commit { - let presentationData = strongSelf.presentationData - let progressSignal = Signal { subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) - self?.present(controller, in: .window(.root)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.8, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() + let actionSheet = ActionSheetController(presentationData: self.presentationData) + var items: [ActionSheetItem] = [] + if havePrivateChats { + //TODO:localize + items.append(ActionSheetButtonItem(title: haveNonPrivateChats ? "Delete from both sides where possible" : "Delete from both sides", color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() - let signal: Signal = strongSelf.context.engine.peers.removePeerChats(peerIds: Array(peerIds)) - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } + guard let strongSelf = self else { + return } - let _ = (signal - |> deliverOnMainQueue).start() strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in var state = state for peerId in peerIds { - state.selectedPeerIds.remove(peerId) + state.pendingRemovalItemIds.insert(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) } return state }) - return true - } else if value == .undo { - strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil)) - strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in - var state = state - for peerId in peerIds { - state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) + let text = strongSelf.presentationData.strings.ChatList_DeletedChats(Int32(peerIds.count)) + + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in + guard let strongSelf = self else { + return false } - return state - }) - self?.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil)) - return true + if value == .commit { + let presentationData = strongSelf.presentationData + let progressSignal = Signal { subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + self?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.8, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + let signal: Signal = strongSelf.context.engine.peers.removePeerChats(peerIds: Array(peerIds), deleteGloballyIfPossible: true) + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + let _ = (signal + |> deliverOnMainQueue).start() + + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in + var state = state + for peerId in peerIds { + state.selectedPeerIds.remove(peerId) + } + return state + }) + + return true + } else if value == .undo { + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil)) + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in + var state = state + for peerId in peerIds { + state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) + } + return state + }) + self?.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil)) + return true + } + return false + }), in: .current) + + strongSelf.donePressed() + })) + } + //TODO:localize + items.append(ActionSheetButtonItem(title: havePrivateChats ? "Delete for me" : self.presentationData.strings.ChatList_DeleteConfirmation(Int32(peerIds.count)), color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let strongSelf = self else { + return } - return false - }), in: .current) - - strongSelf.donePressed() - })) - - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() + + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in + var state = state + for peerId in peerIds { + state.pendingRemovalItemIds.insert(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) + } + return state }) + + let text = strongSelf.presentationData.strings.ChatList_DeletedChats(Int32(peerIds.count)) + + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in + guard let strongSelf = self else { + return false + } + if value == .commit { + let presentationData = strongSelf.presentationData + let progressSignal = Signal { subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + self?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.8, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + let signal: Signal = strongSelf.context.engine.peers.removePeerChats(peerIds: Array(peerIds)) + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + let _ = (signal + |> deliverOnMainQueue).start() + + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in + var state = state + for peerId in peerIds { + state.selectedPeerIds.remove(peerId) + } + return state + }) + + return true + } else if value == .undo { + strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil)) + strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in + var state = state + for peerId in peerIds { + state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) + } + return state + }) + self?.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil)) + return true + } + return false + }), in: .current) + + strongSelf.donePressed() + })) + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) ]) - ]) - self.present(actionSheet, in: .window(.root)) + self.present(actionSheet, in: .window(.root)) + }) } } else if case .middle = action { switch self.chatListDisplayNode.effectiveContainerNode.location { @@ -6858,7 +6970,7 @@ private final class ChatListLocationContext { if let channel = peerView.peers[peerView.peerId] as? TelegramChannel, !channel.flags.contains(.isForum) { if let parentController = self.parentController, let navigationController = parentController.navigationController as? NavigationController { - let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default)) + let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) navigationController.replaceController(parentController, with: chatController, animated: true) } } else { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 9a888199cd8..64cb44c6be3 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1165,7 +1165,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { return } - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) (controller.navigationController as? NavigationController)?.replaceController(controller, with: chatController, animated: false) } @@ -1317,7 +1317,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { } } - private func updateNavigationBar(layout: ContainerViewLayout, deferScrollApplication: Bool, transition: Transition) -> (navigationHeight: CGFloat, storiesInset: CGFloat) { + private func updateNavigationBar(layout: ContainerViewLayout, deferScrollApplication: Bool, transition: ComponentTransition) -> (navigationHeight: CGFloat, storiesInset: CGFloat) { let headerContent = self.controller?.updateHeaderContent() var tabsNode: ASDisplayNode? @@ -1460,7 +1460,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { } if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { - navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: allowAvatarsExpansion, forceUpdate: false, transition: Transition(transition).withUserData(ChatListNavigationBar.AnimationHint( + navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: allowAvatarsExpansion, forceUpdate: false, transition: ComponentTransition(transition).withUserData(ChatListNavigationBar.AnimationHint( disableStoriesAnimations: self.tempDisableStoriesAnimations, crossfadeStoryPeers: false ))) @@ -1475,7 +1475,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { transition.updateSublayerTransformOffset(layer: self.mainContainerNode.layer, offset: CGPoint(x: 0.0, y: -mainDelta)) } - func requestNavigationBarLayout(transition: Transition) { + func requestNavigationBarLayout(transition: ComponentTransition) { guard let (layout, _, _, _, _) = self.containerLayout else { return } @@ -1506,7 +1506,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { var cleanNavigationBarHeight = cleanNavigationBarHeight var storiesInset = storiesInset - let navigationBarLayout = self.updateNavigationBar(layout: layout, deferScrollApplication: true, transition: Transition(transition)) + let navigationBarLayout = self.updateNavigationBar(layout: layout, deferScrollApplication: true, transition: ComponentTransition(transition)) self.mainContainerNode.initialScrollingOffset = ChatListNavigationBar.searchScrollHeight + navigationBarLayout.storiesInset navigationBarHeight = navigationBarLayout.navigationHeight @@ -1637,7 +1637,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { navigationBarComponentView.deferScrollApplication = false - navigationBarComponentView.applyCurrentScroll(transition: Transition(transition)) + navigationBarComponentView.applyCurrentScroll(transition: ComponentTransition(transition)) } } @@ -1738,7 +1738,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { func willScrollToTop() { if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { - navigationBarComponentView.applyScroll(offset: 0.0, allowAvatarsExpansion: false, transition: Transition(animation: .curve(duration: 0.3, curve: .slide))) + navigationBarComponentView.applyScroll(offset: 0.0, allowAvatarsExpansion: false, transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .slide))) } } diff --git a/submodules/ChatListUI/Sources/ChatListEmptyNode.swift b/submodules/ChatListUI/Sources/ChatListEmptyNode.swift index b57a74f469b..c8c78b3c3f0 100644 --- a/submodules/ChatListUI/Sources/ChatListEmptyNode.swift +++ b/submodules/ChatListUI/Sources/ChatListEmptyNode.swift @@ -316,7 +316,7 @@ final class ChatListEmptyNode: ASDisplayNode { self.emptyArchive = emptyArchive } let emptyArchiveSize = emptyArchive.update( - transition: Transition(transition), + transition: ComponentTransition(transition), component: AnyComponent(ArchiveInfoContentComponent( theme: self.theme, strings: self.strings, diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index b45661aca5b..50e23a3115a 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -1530,7 +1530,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi }) }, peerContextAction: { peer, node, gesture, location in - let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing)) + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) let presentationData = context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 081f1d4d6ba..fbb33bdad33 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -1573,7 +1573,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo proceed(chatController) }) } else { - proceed(strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default))) + proceed(strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)) } strongSelf.updateState { state in diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 1ab62f620fe..3c8642e60f5 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -1091,6 +1091,127 @@ private struct DownloadItem: Equatable { } } +private func filteredPeerSearchQueryResults(value: ([FoundPeer], [FoundPeer]), scope: TelegramSearchPeersScope) -> ([FoundPeer], [FoundPeer]) { + switch scope { + case .everywhere: + return value + case .channels: + return ( + value.0.filter { peer in + if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info { + return true + } else { + return false + } + }, + value.1.filter { peer in + if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info { + return true + } else { + return false + } + } + ) + } +} + +final class GlobalPeerSearchContext { + private struct SearchKey: Hashable { + var query: String + + init(query: String) { + self.query = query + } + } + + private final class QueryContext { + var value: ([FoundPeer], [FoundPeer])? + let subscribers = Bag<(TelegramSearchPeersScope, (([FoundPeer], [FoundPeer])) -> Void)>() + let disposable = MetaDisposable() + + init() { + } + + deinit { + self.disposable.dispose() + } + } + + private final class Impl { + private let queue: Queue + private var queryContexts: [SearchKey: QueryContext] = [:] + + init(queue: Queue) { + self.queue = queue + } + + func searchRemotePeers(engine: TelegramEngine, query: String, scope: TelegramSearchPeersScope, onNext: @escaping (([FoundPeer], [FoundPeer])) -> Void) -> Disposable { + let searchKey = SearchKey(query: query) + let queryContext: QueryContext + if let current = self.queryContexts[searchKey] { + queryContext = current + + if let value = queryContext.value { + onNext(filteredPeerSearchQueryResults(value: value, scope: scope)) + } + } else { + queryContext = QueryContext() + self.queryContexts[searchKey] = queryContext + queryContext.disposable.set((engine.contacts.searchRemotePeers( + query: query, + scope: .everywhere + ) + |> delay(0.4, queue: Queue.mainQueue()) + |> deliverOn(self.queue)).start(next: { [weak queryContext] value in + guard let queryContext else { + return + } + queryContext.value = value + for (scope, f) in queryContext.subscribers.copyItems() { + f(filteredPeerSearchQueryResults(value: value, scope: scope)) + } + })) + } + + let index = queryContext.subscribers.add((scope, onNext)) + + let queue = self.queue + return ActionDisposable { [weak self, weak queryContext] in + queue.async { + guard let self, let queryContext else { + return + } + guard let currentContext = self.queryContexts[searchKey], queryContext === queryContext else { + return + } + currentContext.subscribers.remove(index) + if currentContext.subscribers.isEmpty { + currentContext.disposable.dispose() + self.queryContexts.removeValue(forKey: searchKey) + } + } + } + } + } + + private let queue: Queue + private let impl: QueueLocalObject + + init() { + let queue = Queue.mainQueue() + self.queue = queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue) + }) + } + + func searchRemotePeers(engine: TelegramEngine, query: String, scope: TelegramSearchPeersScope = .everywhere) -> Signal<([FoundPeer], [FoundPeer]), NoError> { + return self.impl.signalWith { impl, subscriber in + return impl.searchRemotePeers(engine: engine, query: query, scope: scope, onNext: subscriber.putNext) + } + } +} + final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private let context: AccountContext private let animationCache: AnimationCache @@ -1099,6 +1220,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private let peersFilter: ChatListNodePeersFilter private let requestPeerType: [ReplyMarkupButtonRequestPeerType]? private var presentationData: PresentationData + private let globalPeerSearchContext: GlobalPeerSearchContext? private let key: ChatListSearchPaneKey private let tagMask: EngineMessage.Tags? private let location: ChatListControllerLocation @@ -1175,7 +1297,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private var searchQueryDisposable: Disposable? private var searchOptionsDisposable: Disposable? - init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { + init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?, globalPeerSearchContext: GlobalPeerSearchContext?) { self.context = context self.animationCache = animationCache self.animationRenderer = animationRenderer @@ -1183,6 +1305,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.key = key self.location = location self.navigationController = navigationController + + let globalPeerSearchContext = globalPeerSearchContext ?? GlobalPeerSearchContext() + + self.globalPeerSearchContext = globalPeerSearchContext var peersFilter = peersFilter if case .forum = location { @@ -1788,18 +1914,16 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { foundRemotePeers = ( .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) |> then( - context.engine.contacts.searchRemotePeers(query: query) + globalPeerSearchContext.searchRemotePeers(engine: context.engine, query: query) |> map { ($0.0, $0.1, false) } - |> delay(0.4, queue: Queue.concurrentDefaultQueue()) ) ) } else if let query = query, case .channels = key { foundRemotePeers = ( .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) |> then( - context.engine.contacts.searchRemotePeers(query: query, scope: .channels) + globalPeerSearchContext.searchRemotePeers(engine: context.engine, query: query, scope: .channels) |> map { ($0.0, $0.1, false) } - |> delay(0.4, queue: Queue.concurrentDefaultQueue()) ) ) } else { diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index 6da789c4829..0b32ff7113a 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -127,10 +127,11 @@ private final class ChatListSearchPendingPane { location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, + globalPeerSearchContext: GlobalPeerSearchContext?, key: ChatListSearchPaneKey, hasBecomeReady: @escaping (ChatListSearchPaneKey) -> Void ) { - let paneNode = ChatListSearchListPaneNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: (key == .chats || key == .topics) ? peersFilter : [], requestPeerType: requestPeerType, location: location, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController) + let paneNode = ChatListSearchListPaneNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: (key == .chats || key == .topics) ? peersFilter : [], requestPeerType: requestPeerType, location: location, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController, globalPeerSearchContext: globalPeerSearchContext) self.pane = ChatListSearchPaneWrapper(key: key, node: paneNode) self.disposable = (paneNode.isReady @@ -156,6 +157,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD private let location: ChatListControllerLocation private let searchQuery: Signal private let searchOptions: Signal + private let globalPeerSearchContext: GlobalPeerSearchContext private let navigationController: NavigationController? var interaction: ChatListSearchInteraction? @@ -198,6 +200,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD self.searchQuery = searchQuery self.searchOptions = searchOptions self.navigationController = navigationController + self.globalPeerSearchContext = GlobalPeerSearchContext() super.init() } @@ -432,6 +435,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD location: self.location, searchQuery: self.searchQuery, searchOptions: self.searchOptions, + globalPeerSearchContext: self.globalPeerSearchContext, key: key, hasBecomeReady: { [weak self] key in let apply: () -> Void = { diff --git a/submodules/ChatListUI/Sources/NicegramButtonComponent.swift b/submodules/ChatListUI/Sources/NicegramButtonComponent.swift index 3cf531df7e9..5e638c06dcc 100644 --- a/submodules/ChatListUI/Sources/NicegramButtonComponent.swift +++ b/submodules/ChatListUI/Sources/NicegramButtonComponent.swift @@ -21,7 +21,7 @@ class NicegramButtonComponent: Component { return AssistantButton() } - func update(view: AssistantButton, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: AssistantButton, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { view.pressed = pressed return view.systemLayoutSizeFitting(availableSize) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index aa09f82870f..dcf9d615326 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -322,7 +322,7 @@ private final class ChatListItemTagListComponent: Component { preconditionFailure() } - func update(component: ChatListItemTagListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChatListItemTagListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { var validIds: [Int32] = [] let spacing: CGFloat = floorToScreenPixels(5.0 * component.sizeFactor) var nextX: CGFloat = 0.0 @@ -391,7 +391,7 @@ private final class ChatListItemTagListComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -884,6 +884,9 @@ private final class ChatListMediaPreviewNode: ASDisplayNode { let signal = mediaGridMessagePhoto(account: self.context.account, userLocation: .peer(self.message.id.peerId), photoReference: .message(message: MessageReference(self.message._asMessage()), media: image), fullRepresentationSize: CGSize(width: 36.0, height: 36.0), blurred: hasSpoiler, synchronousLoad: synchronousLoads) self.imageNode.setSignal(signal, attemptSynchronously: synchronousLoads) } + } else { + let signal = chatSecretPhoto(account: self.context.account, userLocation: .peer(self.message.id.peerId), photoReference: .standalone(media: image), ignoreFullSize: true, synchronousLoad: synchronousLoads) + self.imageNode.setSignal(signal, attemptSynchronously: synchronousLoads) } } else if case let .action(action) = self.media, case let .suggestedProfilePhoto(image) = action.action, let image = image { isRound = true @@ -2636,7 +2639,36 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } inner: for media in message.media { - if let image = media as? TelegramMediaImage { + if let paidContent = media as? TelegramMediaPaidContent { + let fitSize = contentImageSize + var index: Int64 = 0 + for media in paidContent.extendedMedia.prefix(3) { + switch media { + case let .preview(dimensions, immediateThumbnailData, videoDuration): + if let immediateThumbnailData { + if let videoDuration { + let thumbnailMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: index), partialReference: nil, resource: EmptyMediaResource(), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Video(duration: Double(videoDuration), size: dimensions ?? PixelDimensions(width: 1, height: 1), flags: [], preloadSize: nil)]) + contentImageSpecs.append(ContentImageSpec(message: message, media: .file(thumbnailMedia), size: fitSize)) + } else { + let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: index), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) + contentImageSpecs.append(ContentImageSpec(message: message, media: .image(thumbnailMedia), size: fitSize)) + } + index += 1 + } + case let .full(fullMedia): + if let image = fullMedia as? TelegramMediaImage { + if let _ = largestImageRepresentation(image.representations) { + contentImageSpecs.append(ContentImageSpec(message: message, media: .image(image), size: fitSize)) + } + } else if let file = fullMedia as? TelegramMediaFile { + if file.isVideo, !file.isVideoSticker, let _ = file.dimensions { + contentImageSpecs.append(ContentImageSpec(message: message, media: .file(file), size: fitSize)) + } + } + } + } + break inner + } else if let image = media as? TelegramMediaImage { if let _ = largestImageRepresentation(image.representations) { let fitSize = contentImageSize contentImageSpecs.append(ContentImageSpec(message: message, media: .image(image), size: fitSize)) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index a1c256698e4..5d0f029f297 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -31,6 +31,24 @@ private func singleMessageType(message: EngineMessage) -> MessageGroupType { return .generic } +private func singleExtendedMediaType(extendedMedia: TelegramExtendedMedia) -> MessageGroupType { + switch extendedMedia { + case let .preview(_, _, videoDuration): + if let _ = videoDuration { + return .videos + } else { + return .photos + } + case let .full(fullMedia): + if let _ = fullMedia as? TelegramMediaImage { + return .photos + } else if let file = fullMedia as? TelegramMediaFile, file.isVideo { + return .videos + } + } + return .generic +} + private func messageGroupType(messages: [EngineMessage]) -> MessageGroupType { if messages.isEmpty { return .generic @@ -45,6 +63,20 @@ private func messageGroupType(messages: [EngineMessage]) -> MessageGroupType { return currentType } +private func paidContentGroupType(paidContent: TelegramMediaPaidContent) -> MessageGroupType { + if paidContent.extendedMedia.isEmpty { + return .generic + } + let currentType = singleExtendedMediaType(extendedMedia: paidContent.extendedMedia[0]) + for i in 1 ..< paidContent.extendedMedia.count { + let nextType = singleExtendedMediaType(extendedMedia: paidContent.extendedMedia[i]) + if nextType != currentType { + return .generic + } + } + return currentType +} + public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, contentSettings: ContentSettings, messages: [EngineMessage], chatPeer: EngineRenderedPeer, accountPeerId: EnginePeer.Id, enableMediaEmoji: Bool = true, isPeerGroup: Bool = false) -> (peer: EnginePeer?, hideAuthor: Bool, messageText: String, spoilers: [NSRange]?, customEmojiRanges: [(NSRange, ChatTextInputTextCustomEmojiAttribute)]?) { let peer: EnginePeer? @@ -76,42 +108,59 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: } } + + let paidContent = message.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent + var textIsReady = false - if messages.count > 1 { - let groupType = messageGroupType(messages: messages) + if messages.count > 1 || (paidContent != nil && (paidContent?.extendedMedia.count ?? 0) > 1) { + let groupType: MessageGroupType + let count: Int32 + if let paidContent { + groupType = paidContentGroupType(paidContent: paidContent) + count = Int32(paidContent.extendedMedia.count) + } else { + groupType = messageGroupType(messages: messages) + count = Int32(messages.count) + } switch groupType { case .photos: if !messageText.isEmpty { textIsReady = true } else { - messageText = strings.ChatList_MessagePhotos(Int32(messages.count)) + messageText = strings.ChatList_MessagePhotos(count) textIsReady = true } case .videos: if !messageText.isEmpty { textIsReady = true } else { - messageText = strings.ChatList_MessageVideos(Int32(messages.count)) + messageText = strings.ChatList_MessageVideos(count) textIsReady = true } case .music: if !messageText.isEmpty { textIsReady = true } else { - messageText = strings.ChatList_MessageMusic(Int32(messages.count)) + messageText = strings.ChatList_MessageMusic(count) textIsReady = true } case .files: if !messageText.isEmpty { textIsReady = true } else { - messageText = strings.ChatList_MessageFiles(Int32(messages.count)) + messageText = strings.ChatList_MessageFiles(count) textIsReady = true } case .generic: var messageTypes = Set() - for message in messages { - messageTypes.insert(singleMessageType(message: message)) + if let paidContent { + for extendedMedia in paidContent.extendedMedia { + messageTypes.insert(singleExtendedMediaType(extendedMedia: extendedMedia)) + } + } else { + for message in messages { + messageTypes.insert(singleMessageType(message: message)) + } } if messageTypes.count == 2 && messageTypes.contains(.photos) && messageTypes.contains(.videos) { if !messageText.isEmpty { @@ -124,6 +173,26 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: if !textIsReady { for media in message.media { switch media { + case let paidContent as TelegramMediaPaidContent: + for extendedMedia in paidContent.extendedMedia { + let type = singleExtendedMediaType(extendedMedia: extendedMedia) + switch type { + case .photos: + if message.text.isEmpty { + messageText = strings.Message_Photo + } else if enableMediaEmoji { + messageText = "🖼 \(messageText)" + } + case .videos: + if message.text.isEmpty { + messageText = strings.Message_Video + } else if enableMediaEmoji { + messageText = "📹 \(messageText)" + } + default: + break + } + } case _ as TelegramMediaImage: if message.text.isEmpty { messageText = strings.Message_Photo diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index 15849fc8fac..0152458b2ce 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -1265,7 +1265,15 @@ public func canSendMessagesToChat(_ state: ChatPresentationInterfaceState) -> Bo return false } } else if case .customChatContents = state.chatLocation { - return true + if case let .customChatContents(contents) = state.subject { + if case .hashTagSearch = contents.kind { + return false + } else { + return true + } + } else { + return true + } } else { return false } diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift index 6c76dab4ed1..a0f301fccf8 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift @@ -145,6 +145,30 @@ public func chatTextInputAddLinkAttribute(_ state: ChatTextInputState, selection } } +public func chatTextInputRemoveLinkAttribute(_ state: ChatTextInputState, selectionRange: Range) -> ChatTextInputState { + if !selectionRange.isEmpty { + let nsRange = NSRange(location: selectionRange.lowerBound, length: selectionRange.count) + var attributesToRemove: [(NSAttributedString.Key, NSRange)] = [] + state.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in + for (key, _) in attributes { + if key == ChatTextInputAttributes.textUrl { + attributesToRemove.append((key, range)) + } else { + attributesToRemove.append((key, nsRange)) + } + } + } + + let result = NSMutableAttributedString(attributedString: state.inputText) + for (attribute, range) in attributesToRemove { + result.removeAttribute(attribute, range: range) + } + return ChatTextInputState(inputText: result, selectionRange: selectionRange) + } else { + return state + } +} + public func chatTextInputAddMentionAttribute(_ state: ChatTextInputState, peer: EnginePeer) -> ChatTextInputState { let inputText = NSMutableAttributedString(attributedString: state.inputText) diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift index 5cc4709e417..402ddcf61aa 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift @@ -16,6 +16,7 @@ public enum SendMessageActionSheetControllerParams { public let isScheduledMessages: Bool public let mediaPreview: ChatSendMessageContextScreenMediaPreview? public let mediaCaptionIsAbove: (Bool, (Bool) -> Void)? + public let messageEffect: (ChatSendMessageActionSheetControllerSendParameters.Effect?, (ChatSendMessageActionSheetControllerSendParameters.Effect?) -> Void)? public let attachment: Bool public let canSendWhenOnline: Bool public let forwardMessageIds: [EngineMessage.Id] @@ -24,6 +25,7 @@ public enum SendMessageActionSheetControllerParams { isScheduledMessages: Bool, mediaPreview: ChatSendMessageContextScreenMediaPreview?, mediaCaptionIsAbove: (Bool, (Bool) -> Void)?, + messageEffect: (ChatSendMessageActionSheetControllerSendParameters.Effect?, (ChatSendMessageActionSheetControllerSendParameters.Effect?) -> Void)?, attachment: Bool, canSendWhenOnline: Bool, forwardMessageIds: [EngineMessage.Id] @@ -31,6 +33,7 @@ public enum SendMessageActionSheetControllerParams { self.isScheduledMessages = isScheduledMessages self.mediaPreview = mediaPreview self.mediaCaptionIsAbove = mediaCaptionIsAbove + self.messageEffect = messageEffect self.attachment = attachment self.canSendWhenOnline = canSendWhenOnline self.forwardMessageIds = forwardMessageIds @@ -57,6 +60,7 @@ public func makeChatSendMessageActionSheetController( // MARK: Nicegram TranslateEnteredMessage nicegramData: ChatSendMessageContextNicegramData = .empty, // + initialData: ChatSendMessageContextScreen.InitialData, context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id?, @@ -79,6 +83,7 @@ public func makeChatSendMessageActionSheetController( // MARK: Nicegram TranslateEnteredMessage nicegramData: nicegramData, // + initialData: initialData, context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift index 95abfb1f301..bef975e7d95 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift @@ -48,10 +48,10 @@ public protocol ChatSendMessageContextScreenMediaPreview: AnyObject { var globalClippingRect: CGRect? { get } var layoutType: ChatSendMessageContextScreenMediaPreviewLayoutType { get } - func animateIn(transition: Transition) - func animateOut(transition: Transition) - func animateOutOnSend(transition: Transition) - func update(containerSize: CGSize, transition: Transition) -> CGSize + func animateIn(transition: ComponentTransition) + func animateOut(transition: ComponentTransition) + func animateOutOnSend(transition: ComponentTransition) + func update(containerSize: CGSize, transition: ComponentTransition) -> CGSize } // MARK: Nicegram TranslateEnteredMessage @@ -84,6 +84,7 @@ final class ChatSendMessageContextScreenComponent: Component { let interlocutorLangCode: String? // + let initialData: ChatSendMessageContextScreen.InitialData let context: AccountContext let updatedPresentationData: (initial: PresentationData, signal: Signal)? let peerId: EnginePeer.Id? @@ -106,6 +107,7 @@ final class ChatSendMessageContextScreenComponent: Component { // MARK: Nicegram TranslateEnteredMessage nicegramData: ChatSendMessageContextNicegramData, // + initialData: ChatSendMessageContextScreen.InitialData, context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id?, @@ -128,6 +130,7 @@ final class ChatSendMessageContextScreenComponent: Component { self.nicegramData = nicegramData self.interlocutorLangCode = getCachedLanguageCode(forChatWith: peerId) // + self.initialData = initialData self.context = context self.updatedPresentationData = updatedPresentationData self.peerId = peerId @@ -305,7 +308,7 @@ final class ChatSendMessageContextScreenComponent: Component { return false } - func update(component: ChatSendMessageContextScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChatSendMessageContextScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -343,7 +346,7 @@ final class ChatSendMessageContextScreenComponent: Component { let messageActionsSpacing: CGFloat = 7.0 - let alphaTransition: Transition + let alphaTransition: ComponentTransition if transition.animation.isImmediate { alphaTransition = .immediate } else { @@ -357,6 +360,8 @@ final class ChatSendMessageContextScreenComponent: Component { switch component.params { case let .sendMessage(sendMessage): self.mediaCaptionIsAbove = sendMessage.mediaCaptionIsAbove?.0 ?? false + + self.selectedMessageEffect = component.initialData.messageEffect case let .editMessage(editMessage): self.mediaCaptionIsAbove = editMessage.mediaCaptionIsAbove?.0 ?? false } @@ -679,6 +684,7 @@ final class ChatSendMessageContextScreenComponent: Component { id: AnyHashable("items"), items: items, reactionItems: nil, + previewReaction: nil, tip: nil, tipSignal: .single(nil), dismissed: nil @@ -695,7 +701,7 @@ final class ChatSendMessageContextScreenComponent: Component { return } if !self.isUpdating { - self.state?.updated(transition: Transition(transition)) + self.state?.updated(transition: ComponentTransition(transition)) } } ) @@ -708,6 +714,7 @@ final class ChatSendMessageContextScreenComponent: Component { id: AnyHashable("items"), items: items, reactionItems: nil, + previewReaction: nil, tip: nil, tipSignal: .single(nil), dismissed: nil @@ -853,7 +860,7 @@ final class ChatSendMessageContextScreenComponent: Component { return } if !self.isUpdating { - self.state?.updated(transition: Transition(transition)) + self.state?.updated(transition: ComponentTransition(transition)) } }, requestLayout: { [weak self] transition in @@ -861,7 +868,7 @@ final class ChatSendMessageContextScreenComponent: Component { return } if !self.isUpdating { - self.state?.updated(transition: Transition(transition)) + self.state?.updated(transition: ComponentTransition(transition)) } }, requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in @@ -924,6 +931,12 @@ final class ChatSendMessageContextScreenComponent: Component { if !self.isUpdating { self.state?.updated(transition: .easeInOut(duration: 0.2)) } + if case let .sendMessage(sendMessage) = component.params { + let mappedEffect = self.selectedMessageEffect.flatMap { + return ChatSendMessageActionSheetControllerSendParameters.Effect(id: $0.id) + } + sendMessage.messageEffect?.1(mappedEffect) + } return } else { self.selectedMessageEffect = messageEffect @@ -932,6 +945,13 @@ final class ChatSendMessageContextScreenComponent: Component { self.state?.updated(transition: .easeInOut(duration: 0.2)) } + if case let .sendMessage(sendMessage) = component.params { + let mappedEffect = self.selectedMessageEffect.flatMap { + return ChatSendMessageActionSheetControllerSendParameters.Effect(id: $0.id) + } + sendMessage.messageEffect?.1(mappedEffect) + } + HapticFeedback().tap() } } else { @@ -941,6 +961,13 @@ final class ChatSendMessageContextScreenComponent: Component { self.state?.updated(transition: .easeInOut(duration: 0.2)) } + if case let .sendMessage(sendMessage) = component.params { + let mappedEffect = self.selectedMessageEffect.flatMap { + return ChatSendMessageActionSheetControllerSendParameters.Effect(id: $0.id) + } + sendMessage.messageEffect?.1(mappedEffect) + } + HapticFeedback().tap() } @@ -1025,12 +1052,14 @@ final class ChatSendMessageContextScreenComponent: Component { standaloneReactionAnimation = DirectAnimatedStickerNode() effectiveScale = 1.4 #else - if "".isEmpty { + standaloneReactionAnimation = DirectAnimatedStickerNode() + effectiveScale = 1.4 + /*if "".isEmpty { standaloneReactionAnimation = DirectAnimatedStickerNode() effectiveScale = 1.4 } else { standaloneReactionAnimation = LottieMetalAnimatedStickerNode() - } + }*/ #endif standaloneReactionAnimation.isUserInteractionEnabled = false @@ -1100,8 +1129,7 @@ final class ChatSendMessageContextScreenComponent: Component { self.animateOutToEmpty = true self.environment?.controller()?.dismiss() - //TODO:localize - let premiumController = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .animatedEmoji, forceDark: false, dismissed: nil) + let premiumController = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .messageEffects, forceDark: false, dismissed: nil) component.openPremiumPaywall(premiumController) } return false @@ -1223,7 +1251,7 @@ final class ChatSendMessageContextScreenComponent: Component { break case .animatedIn: transition.setAlpha(view: actionsStackNode.view, alpha: 1.0) - Transition.immediate.setScale(view: actionsStackNode.view, scale: 1.0) + ComponentTransition.immediate.setScale(view: actionsStackNode.view, scale: 1.0) actionsStackNode.layer.animateSpring(from: 0.001 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0) messageItemView.animateIn( @@ -1396,12 +1424,20 @@ final class ChatSendMessageContextScreenComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } public class ChatSendMessageContextScreen: ViewControllerComponentContainer, ChatSendMessageActionSheetController { + public final class InitialData { + fileprivate let messageEffect: AvailableMessageEffects.MessageEffect? + + init(messageEffect: AvailableMessageEffects.MessageEffect?) { + self.messageEffect = messageEffect + } + } + private let context: AccountContext private var processedDidAppear: Bool = false @@ -1421,6 +1457,7 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha // MARK: Nicegram TranslateEnteredMessage nicegramData: ChatSendMessageContextNicegramData, // + initialData: InitialData, context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id?, @@ -1447,6 +1484,7 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha // MARK: Nicegram TranslateEnteredMessage nicegramData: nicegramData, // + initialData: initialData, context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, @@ -1528,4 +1566,30 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha } } } + + public static func initialData(context: AccountContext, currentMessageEffectId: Int64?) -> Signal { + let messageEffect: Signal + if let currentMessageEffectId { + messageEffect = context.engine.stickers.availableMessageEffects() + |> take(1) + |> map { availableMessageEffects -> AvailableMessageEffects.MessageEffect? in + guard let availableMessageEffects else { + return nil + } + for messageEffect in availableMessageEffects.messageEffects { + if messageEffect.id == currentMessageEffectId || messageEffect.effectSticker.fileId.id == currentMessageEffectId { + return messageEffect + } + } + return nil + } + } else { + messageEffect = .single(nil) + } + + return messageEffect + |> map { messageEffect -> InitialData in + return InitialData(messageEffect: messageEffect) + } + } } diff --git a/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift b/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift index cf48ea62ba0..3fddcea6293 100644 --- a/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift +++ b/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift @@ -20,16 +20,16 @@ import MultilineTextComponent import ChatInputTextNode import EmojiTextAttachmentView -private final class EffectIcon: Component { - enum Content: Equatable { +public final class ChatSendMessageScreenEffectIcon: Component { + public enum Content: Equatable { case file(TelegramMediaFile) case text(String) } - let context: AccountContext - let content: Content + public let context: AccountContext + public let content: Content - init( + public init( context: AccountContext, content: Content ) { @@ -37,7 +37,7 @@ private final class EffectIcon: Component { self.content = content } - static func ==(lhs: EffectIcon, rhs: EffectIcon) -> Bool { + public static func ==(lhs: ChatSendMessageScreenEffectIcon, rhs: ChatSendMessageScreenEffectIcon) -> Bool { if lhs.context !== rhs.context { return false } @@ -47,19 +47,19 @@ private final class EffectIcon: Component { return true } - final class View: UIView { + public final class View: UIView { private var fileView: ReactionIconView? private var textView: ComponentView? - override init(frame: CGRect) { + override public init(frame: CGRect) { super.init(frame: frame) } - required init?(coder: NSCoder) { + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func update(component: EffectIcon, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChatSendMessageScreenEffectIcon, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if case let .file(file) = component.content { let fileView: ReactionIconView if let current = self.fileView { @@ -126,11 +126,11 @@ private final class EffectIcon: Component { } } - func makeView() -> View { + public func makeView() -> View { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -246,7 +246,7 @@ final class MessageItemView: UIView { func animateIn( sourceTextInputView: ChatInputTextView?, isEditMessage: Bool, - transition: Transition + transition: ComponentTransition ) { if isEditMessage { transition.animateScale(view: self, from: 0.001, to: 1.0) @@ -262,7 +262,7 @@ final class MessageItemView: UIView { sourceTextInputView: ChatInputTextView?, toEmpty: Bool, isEditMessage: Bool, - transition: Transition + transition: ComponentTransition ) { if isEditMessage { transition.setScale(view: self, scale: 0.001) @@ -294,7 +294,7 @@ final class MessageItemView: UIView { containerSize: CGSize, effect: AvailableMessageEffects.MessageEffect?, isEditMessage: Bool, - transition: Transition + transition: ComponentTransition ) -> CGSize { self.emojiViewProvider = emojiViewProvider @@ -307,7 +307,7 @@ final class MessageItemView: UIView { effectIcon = ComponentView() self.effectIcon = effectIcon } - let effectIconContent: EffectIcon.Content + let effectIconContent: ChatSendMessageScreenEffectIcon.Content if let staticIcon = effect.staticIcon { effectIconContent = .file(staticIcon) } else { @@ -315,7 +315,7 @@ final class MessageItemView: UIView { } effectIconSize = effectIcon.update( transition: .immediate, - component: AnyComponent(EffectIcon( + component: AnyComponent(ChatSendMessageScreenEffectIcon( context: context, content: effectIconContent )), @@ -351,7 +351,7 @@ final class MessageItemView: UIView { backgroundNode: backgroundNode ) - let alphaTransition: Transition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25) + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25) if let sourceMediaPreview { let mediaPreviewClippingView: UIView @@ -423,6 +423,11 @@ final class MessageItemView: UIView { } let messageAttributedText = NSMutableAttributedString(attributedString: textString) + + for entity in generateTextEntities(textString.string, enabledTypes: .all) { + messageAttributedText.addAttribute(.foregroundColor, value: presentationData.theme.chat.message.outgoing.linkTextColor, range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + } + textNode.attributedText = messageAttributedText } @@ -620,6 +625,11 @@ final class MessageItemView: UIView { } let messageAttributedText = NSMutableAttributedString(attributedString: textString) + + for entity in generateTextEntities(textString.string, enabledTypes: .all) { + messageAttributedText.addAttribute(.foregroundColor, value: presentationData.theme.chat.message.outgoing.linkTextColor, range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + } + textNode.attributedText = messageAttributedText } @@ -764,7 +774,7 @@ final class MessageItemView: UIView { isAnimatedIn: Bool, localFrame: CGRect, containerSize: CGSize, - transition: Transition + transition: ComponentTransition ) { if let mediaPreviewClippingView = self.mediaPreviewClippingView, let sourceMediaPreview { let clippingFrame: CGRect diff --git a/submodules/ChatSendMessageActionUI/Sources/SendButton.swift b/submodules/ChatSendMessageActionUI/Sources/SendButton.swift index 6d637852d19..74ca91380de 100644 --- a/submodules/ChatSendMessageActionUI/Sources/SendButton.swift +++ b/submodules/ChatSendMessageActionUI/Sources/SendButton.swift @@ -32,7 +32,7 @@ final class SendButton: HighlightTrackingButton { private let iconView: UIImageView private var activityIndicator: RadialStatusNode? - private var didProcessSourceCustomContent: Bool = false + private var previousIsAnimatedIn: Bool? private var sourceCustomContentView: UIView? init(kind: Kind) { @@ -67,10 +67,11 @@ final class SendButton: HighlightTrackingButton { isAnimatedIn: Bool, isLoadingEffectAnimation: Bool, size: CGSize, - transition: Transition + transition: ComponentTransition ) { let innerSize = CGSize(width: size.width - 5.5 * 2.0, height: 33.0) - transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - innerSize.width) * 0.5), y: floorToScreenPixels((size.height - innerSize.height) * 0.5)), size: innerSize)) + let containerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - innerSize.width) * 0.5), y: floorToScreenPixels((size.height - innerSize.height) * 0.5)), size: innerSize) + transition.setFrame(view: self.containerView, frame: containerFrame) transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: innerSize.height * 0.5) if self.window != nil { @@ -97,13 +98,21 @@ final class SendButton: HighlightTrackingButton { self.backgroundLayer.backgroundColor = presentationData.theme.chat.inputPanel.actionControlFillColor.cgColor transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: CGPoint(), size: innerSize)) - if !self.didProcessSourceCustomContent { - self.didProcessSourceCustomContent = true + if self.previousIsAnimatedIn != isAnimatedIn { + self.previousIsAnimatedIn = isAnimatedIn + + var sourceCustomContentViewAlpha: CGFloat = 1.0 + if let sourceCustomContentView = self.sourceCustomContentView { + sourceCustomContentViewAlpha = sourceCustomContentView.alpha + sourceCustomContentView.removeFromSuperview() + self.sourceCustomContentView = nil + } if let sourceSendButton = sourceSendButton as? ChatSendMessageActionSheetControllerSourceSendButtonNode { if let sourceCustomContentView = sourceSendButton.makeCustomContents() { self.sourceCustomContentView = sourceCustomContentView - self.iconView.superview?.insertSubview(sourceCustomContentView, belowSubview: self.iconView) + sourceCustomContentView.alpha = sourceCustomContentViewAlpha + self.addSubview(sourceCustomContentView) } } } @@ -118,11 +127,16 @@ final class SendButton: HighlightTrackingButton { } if let sourceCustomContentView = self.sourceCustomContentView { + var sourceCustomContentTransition = transition + if sourceCustomContentView.bounds.isEmpty { + sourceCustomContentTransition = .immediate + } + let sourceCustomContentSize = sourceCustomContentView.bounds.size - let sourceCustomContentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((innerSize.width - sourceCustomContentSize.width) * 0.5) + UIScreenPixel, y: floorToScreenPixels((innerSize.height - sourceCustomContentSize.height) * 0.5)), size: sourceCustomContentSize) - transition.setPosition(view: sourceCustomContentView, position: sourceCustomContentFrame.center) - transition.setBounds(view: sourceCustomContentView, bounds: CGRect(origin: CGPoint(), size: sourceCustomContentFrame.size)) - transition.setAlpha(view: sourceCustomContentView, alpha: isAnimatedIn ? 0.0 : 1.0) + let sourceCustomContentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((innerSize.width - sourceCustomContentSize.width) * 0.5) + UIScreenPixel, y: floorToScreenPixels((innerSize.height - sourceCustomContentSize.height) * 0.5)), size: sourceCustomContentSize).offsetBy(dx: containerFrame.minX, dy: containerFrame.minY) + sourceCustomContentTransition.setPosition(view: sourceCustomContentView, position: sourceCustomContentFrame.center) + sourceCustomContentTransition.setBounds(view: sourceCustomContentView, bounds: CGRect(origin: CGPoint(), size: sourceCustomContentFrame.size)) + sourceCustomContentTransition.setAlpha(view: sourceCustomContentView, alpha: isAnimatedIn ? 0.0 : 1.0) } if let icon = self.iconView.image { @@ -183,7 +197,7 @@ final class SendButton: HighlightTrackingButton { } } - func updateGlobalRect(rect: CGRect, within containerSize: CGSize, transition: Transition) { + func updateGlobalRect(rect: CGRect, within containerSize: CGSize, transition: ComponentTransition) { if let backgroundContent = self.backgroundContent { backgroundContent.update(rect: CGRect(origin: CGPoint(x: rect.minX + self.containerView.frame.minX, y: rect.minY + self.containerView.frame.minY), size: backgroundContent.bounds.size), within: containerSize, transition: transition.containedViewLayoutTransition) } diff --git a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift index 85e4268977a..8ed99a1fb87 100644 --- a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift +++ b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift @@ -176,11 +176,13 @@ private final class ChatTextLinkEditAlertContentNode: AlertContentNode { } private var isEditing = false + private let allowEmpty: Bool - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, link: String?) { + init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, link: String?, allowEmpty: Bool) { self.strings = strings self.text = text self.isEditing = link != nil + self.allowEmpty = allowEmpty self.titleNode = ASTextNode() self.titleNode.maximumNumberOfLines = 2 @@ -220,6 +222,9 @@ private final class ChatTextLinkEditAlertContentNode: AlertContentNode { self.addSubnode(actionNode) } self.actionNodes.last?.actionEnabled = !(link ?? "").isEmpty + if allowEmpty { + self.actionNodes.last?.actionEnabled = true + } for separatorNode in self.actionVerticalSeparators { self.addSubnode(separatorNode) @@ -235,7 +240,11 @@ private final class ChatTextLinkEditAlertContentNode: AlertContentNode { self.inputFieldNode.textChanged = { [weak self] text in if let strongSelf = self, let lastNode = strongSelf.actionNodes.last { - lastNode.actionEnabled = !text.isEmpty + if strongSelf.allowEmpty { + lastNode.actionEnabled = true + } else { + lastNode.actionEnabled = !text.isEmpty + } } } @@ -402,7 +411,7 @@ private final class ChatTextLinkEditAlertContentNode: AlertContentNode { } } -public func chatTextLinkEditController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, account: Account, text: String, link: String?, apply: @escaping (String?) -> Void) -> AlertController { +public func chatTextLinkEditController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, account: Account, text: String, link: String?, allowEmpty: Bool = false, apply: @escaping (String?) -> Void) -> AlertController { let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 } var dismissImpl: ((Bool) -> Void)? @@ -415,7 +424,7 @@ public func chatTextLinkEditController(sharedContext: SharedAccountContext, upda applyImpl?() })] - let contentNode = ChatTextLinkEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, link: link) + let contentNode = ChatTextLinkEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, link: link, allowEmpty: allowEmpty) contentNode.complete = { applyImpl?() } @@ -427,6 +436,9 @@ public func chatTextLinkEditController(sharedContext: SharedAccountContext, upda if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true, "tg": false, "ton": false]) { dismissImpl?(true) apply(updatedLink) + } else if allowEmpty && contentNode.link.isEmpty { + dismissImpl?(true) + apply("") } else { contentNode.animateError() } diff --git a/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift b/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift index 6871454b3b3..043415132b6 100644 --- a/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift +++ b/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift @@ -1,9 +1,9 @@ import Foundation import UIKit -public extension Transition.Appear { - static func `default`(scale: Bool = false, alpha: Bool = false) -> Transition.Appear { - return Transition.Appear { component, view, transition in +public extension ComponentTransition.Appear { + static func `default`(scale: Bool = false, alpha: Bool = false) -> ComponentTransition.Appear { + return ComponentTransition.Appear { component, view, transition in if scale { transition.animateScale(view: view, from: 0.01, to: 1.0) } @@ -13,16 +13,16 @@ public extension Transition.Appear { } } - static func scaleIn() -> Transition.Appear { - return Transition.Appear { component, view, transition in + static func scaleIn() -> ComponentTransition.Appear { + return ComponentTransition.Appear { component, view, transition in transition.animateScale(view: view, from: 0.01, to: 1.0) } } } -public extension Transition.AppearWithGuide { - static func `default`(scale: Bool = false, alpha: Bool = false) -> Transition.AppearWithGuide { - return Transition.AppearWithGuide { component, view, guide, transition in +public extension ComponentTransition.AppearWithGuide { + static func `default`(scale: Bool = false, alpha: Bool = false) -> ComponentTransition.AppearWithGuide { + return ComponentTransition.AppearWithGuide { component, view, guide, transition in if scale { transition.animateScale(view: view, from: 0.01, to: 1.0) } @@ -34,9 +34,9 @@ public extension Transition.AppearWithGuide { } } -public extension Transition.Disappear { - static func `default`(scale: Bool = false, alpha: Bool = true) -> Transition.Disappear { - return Transition.Disappear { view, transition, completion in +public extension ComponentTransition.Disappear { + static func `default`(scale: Bool = false, alpha: Bool = true) -> ComponentTransition.Disappear { + return ComponentTransition.Disappear { view, transition, completion in if scale { transition.setScale(view: view, scale: 0.01, completion: { _ in if !alpha { @@ -56,9 +56,9 @@ public extension Transition.Disappear { } } -public extension Transition.DisappearWithGuide { - static func `default`(alpha: Bool = true) -> Transition.DisappearWithGuide { - return Transition.DisappearWithGuide { stage, view, guide, transition, completion in +public extension ComponentTransition.DisappearWithGuide { + static func `default`(alpha: Bool = true) -> ComponentTransition.DisappearWithGuide { + return ComponentTransition.DisappearWithGuide { stage, view, guide, transition, completion in switch stage { case .begin: if alpha { @@ -78,8 +78,8 @@ public extension Transition.DisappearWithGuide { } } -public extension Transition.Update { - static let `default` = Transition.Update { component, view, transition in +public extension ComponentTransition.Update { + static let `default` = ComponentTransition.Update { component, view, transition in let frame = component.size.centered(around: component._position ?? CGPoint()) if let scale = component._scale { transition.setBounds(view: view, bounds: CGRect(origin: CGPoint(), size: frame.size)) diff --git a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift index 04df0fd8cea..4f43d6cff35 100644 --- a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift +++ b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift @@ -6,7 +6,7 @@ private func updateChildAnyComponent( component: AnyComponent, view: UIView, availableSize: CGSize, - transition: Transition + transition: ComponentTransition ) -> _UpdatedChildComponent { let parentContext = _AnyCombinedComponentContext.current @@ -85,7 +85,7 @@ public final class _ConcreteChildComponent: _AnyChildC return .direct(self.directId) } - public func update(component: ComponentType, @EnvironmentBuilder environment: () -> Environment, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent { + public func update(component: ComponentType, @EnvironmentBuilder environment: () -> Environment, availableSize: CGSize, transition: ComponentTransition) -> _UpdatedChildComponent { let parentContext = _AnyCombinedComponentContext.current if !parentContext.updateContext.configuredViews.insert(self.id).inserted { preconditionFailure("Child component can only be configured once") @@ -119,7 +119,7 @@ public final class _ConcreteChildComponent: _AnyChildC } public extension _ConcreteChildComponent where ComponentType.EnvironmentType == Empty { - func update(component: ComponentType, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent { + func update(component: ComponentType, availableSize: CGSize, transition: ComponentTransition) -> _UpdatedChildComponent { return self.update(component: component, environment: {}, availableSize: availableSize, transition: transition) } } @@ -141,7 +141,7 @@ public final class _ChildComponentGuide { return .direct(self.directId) } - public func update(position: CGPoint, transition: Transition) -> _UpdatedChildComponentGuide { + public func update(position: CGPoint, transition: ComponentTransition) -> _UpdatedChildComponentGuide { let parentContext = _AnyCombinedComponentContext.current let previousPosition = parentContext.guides[self.id] @@ -182,11 +182,11 @@ public final class _UpdatedChildComponent { var _clipsToBounds: Bool? var _shadow: Shadow? - fileprivate var transitionAppear: Transition.Appear? - fileprivate var transitionAppearWithGuide: (Transition.AppearWithGuide, _AnyChildComponent.Id)? - fileprivate var transitionDisappear: Transition.Disappear? - fileprivate var transitionDisappearWithGuide: (Transition.DisappearWithGuide, _AnyChildComponent.Id)? - fileprivate var transitionUpdate: Transition.Update? + fileprivate var transitionAppear: ComponentTransition.Appear? + fileprivate var transitionAppearWithGuide: (ComponentTransition.AppearWithGuide, _AnyChildComponent.Id)? + fileprivate var transitionDisappear: ComponentTransition.Disappear? + fileprivate var transitionDisappearWithGuide: (ComponentTransition.DisappearWithGuide, _AnyChildComponent.Id)? + fileprivate var transitionUpdate: ComponentTransition.Update? fileprivate var gestures: [Gesture] = [] fileprivate init( @@ -203,31 +203,31 @@ public final class _UpdatedChildComponent { self.size = size } - @discardableResult public func appear(_ transition: Transition.Appear) -> _UpdatedChildComponent { + @discardableResult public func appear(_ transition: ComponentTransition.Appear) -> _UpdatedChildComponent { self.transitionAppear = transition self.transitionAppearWithGuide = nil return self } - @discardableResult public func appear(_ transition: Transition.AppearWithGuide, guide: _UpdatedChildComponentGuide) -> _UpdatedChildComponent { + @discardableResult public func appear(_ transition: ComponentTransition.AppearWithGuide, guide: _UpdatedChildComponentGuide) -> _UpdatedChildComponent { self.transitionAppear = nil self.transitionAppearWithGuide = (transition, guide.instance.id) return self } - @discardableResult public func disappear(_ transition: Transition.Disappear) -> _UpdatedChildComponent { + @discardableResult public func disappear(_ transition: ComponentTransition.Disappear) -> _UpdatedChildComponent { self.transitionDisappear = transition self.transitionDisappearWithGuide = nil return self } - @discardableResult public func disappear(_ transition: Transition.DisappearWithGuide, guide: _UpdatedChildComponentGuide) -> _UpdatedChildComponent { + @discardableResult public func disappear(_ transition: ComponentTransition.DisappearWithGuide, guide: _UpdatedChildComponentGuide) -> _UpdatedChildComponent { self.transitionDisappear = nil self.transitionDisappearWithGuide = (transition, guide.instance.id) return self } - @discardableResult public func update(_ transition: Transition.Update) -> _UpdatedChildComponent { + @discardableResult public func update(_ transition: ComponentTransition.Update) -> _UpdatedChildComponent { self.transitionUpdate = transition return self } @@ -278,7 +278,7 @@ public final class _EnvironmentChildComponent: _AnyChildCompone return .direct(self.directId) } - func update(component: AnyComponent, @EnvironmentBuilder environment: () -> Environment, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent { + func update(component: AnyComponent, @EnvironmentBuilder environment: () -> Environment, availableSize: CGSize, transition: ComponentTransition) -> _UpdatedChildComponent { let parentContext = _AnyCombinedComponentContext.current if !parentContext.updateContext.configuredViews.insert(self.id).inserted { preconditionFailure("Child component can only be configured once") @@ -312,17 +312,17 @@ public final class _EnvironmentChildComponent: _AnyChildCompone } public extension _EnvironmentChildComponent where EnvironmentType == Empty { - func update(component: AnyComponent, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent { + func update(component: AnyComponent, availableSize: CGSize, transition: ComponentTransition) -> _UpdatedChildComponent { return self.update(component: component, environment: {}, availableSize: availableSize, transition: transition) } } public extension _EnvironmentChildComponent { - func update(_ component: ComponentType, @EnvironmentBuilder environment: () -> Environment, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent where ComponentType.EnvironmentType == EnvironmentType { + func update(_ component: ComponentType, @EnvironmentBuilder environment: () -> Environment, availableSize: CGSize, transition: ComponentTransition) -> _UpdatedChildComponent where ComponentType.EnvironmentType == EnvironmentType { return self.update(component: AnyComponent(component), environment: environment, availableSize: availableSize, transition: transition) } - func update(_ component: ComponentType, @EnvironmentBuilder environment: () -> Environment, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent where ComponentType.EnvironmentType == EnvironmentType, EnvironmentType == Empty { + func update(_ component: ComponentType, @EnvironmentBuilder environment: () -> Environment, availableSize: CGSize, transition: ComponentTransition) -> _UpdatedChildComponent where ComponentType.EnvironmentType == EnvironmentType, EnvironmentType == Empty { return self.update(component: AnyComponent(component), environment: {}, availableSize: availableSize, transition: transition) } } @@ -334,7 +334,7 @@ public final class _EnvironmentChildComponentFromMap: _AnyChild self.id = id } - public func update(component: AnyComponent, @EnvironmentBuilder environment: () -> Environment, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent { + public func update(component: AnyComponent, @EnvironmentBuilder environment: () -> Environment, availableSize: CGSize, transition: ComponentTransition) -> _UpdatedChildComponent { let parentContext = _AnyCombinedComponentContext.current if !parentContext.updateContext.configuredViews.insert(self.id).inserted { preconditionFailure("Child component can only be configured once") @@ -368,7 +368,7 @@ public final class _EnvironmentChildComponentFromMap: _AnyChild } public extension _EnvironmentChildComponentFromMap where EnvironmentType == Empty { - func update(component: AnyComponent, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent { + func update(component: AnyComponent, availableSize: CGSize, transition: ComponentTransition) -> _UpdatedChildComponent { return self.update(component: component, environment: {}, availableSize: availableSize, transition: transition) } } @@ -393,7 +393,7 @@ public final class CombinedComponentContext { public let component: ComponentType public let availableSize: CGSize - public let transition: Transition + public let transition: ComponentTransition private let addImpl: (_ updatedComponent: _UpdatedChildComponent) -> Void public var environment: Environment { @@ -408,7 +408,7 @@ public final class CombinedComponentContext { view: UIView, component: ComponentType, availableSize: CGSize, - transition: Transition, + transition: ComponentTransition, add: @escaping (_ updatedComponent: _UpdatedChildComponent) -> Void ) { self.context = context @@ -467,8 +467,8 @@ private class _AnyCombinedComponentContext { class ChildView { let view: UIView var index: Int - var transition: Transition.Disappear? - var transitionWithGuide: (Transition.DisappearWithGuide, _AnyChildComponent.Id)? + var transition: ComponentTransition.Disappear? + var transitionWithGuide: (ComponentTransition.DisappearWithGuide, _AnyChildComponent.Id)? var gestures: [UInt: UIGestureRecognizer] = [:] @@ -507,15 +507,15 @@ private class _AnyCombinedComponentContext { class DisappearingChildView { let view: UIView let guideId: _AnyChildComponent.Id? - let transition: Transition.Disappear? - let transitionWithGuide: Transition.DisappearWithGuide? + let transition: ComponentTransition.Disappear? + let transitionWithGuide: ComponentTransition.DisappearWithGuide? let completion: () -> Void init( view: UIView, guideId: _AnyChildComponent.Id?, - transition: Transition.Disappear?, - transitionWithGuide: Transition.DisappearWithGuide?, + transition: ComponentTransition.Disappear?, + transitionWithGuide: ComponentTransition.DisappearWithGuide?, completion: @escaping () -> Void ) { self.view = view @@ -555,39 +555,39 @@ private extension UIView { } } -public extension Transition { +public extension ComponentTransition { final class Appear { - private let f: (_UpdatedChildComponent, UIView, Transition) -> Void + private let f: (_UpdatedChildComponent, UIView, ComponentTransition) -> Void - public init(_ f: @escaping (_UpdatedChildComponent, UIView, Transition) -> Void) { + public init(_ f: @escaping (_UpdatedChildComponent, UIView, ComponentTransition) -> Void) { self.f = f } - public func callAsFunction(component: _UpdatedChildComponent, view: UIView, transition: Transition) { + public func callAsFunction(component: _UpdatedChildComponent, view: UIView, transition: ComponentTransition) { self.f(component, view, transition) } } final class AppearWithGuide { - private let f: (_UpdatedChildComponent, UIView, CGPoint, Transition) -> Void + private let f: (_UpdatedChildComponent, UIView, CGPoint, ComponentTransition) -> Void - public init(_ f: @escaping (_UpdatedChildComponent, UIView, CGPoint, Transition) -> Void) { + public init(_ f: @escaping (_UpdatedChildComponent, UIView, CGPoint, ComponentTransition) -> Void) { self.f = f } - public func callAsFunction(component: _UpdatedChildComponent, view: UIView, guide: CGPoint, transition: Transition) { + public func callAsFunction(component: _UpdatedChildComponent, view: UIView, guide: CGPoint, transition: ComponentTransition) { self.f(component, view, guide, transition) } } final class Disappear { - private let f: (UIView, Transition, @escaping () -> Void) -> Void + private let f: (UIView, ComponentTransition, @escaping () -> Void) -> Void - public init(_ f: @escaping (UIView, Transition, @escaping () -> Void) -> Void) { + public init(_ f: @escaping (UIView, ComponentTransition, @escaping () -> Void) -> Void) { self.f = f } - public func callAsFunction(view: UIView, transition: Transition, completion: @escaping () -> Void) { + public func callAsFunction(view: UIView, transition: ComponentTransition, completion: @escaping () -> Void) { self.f(view, transition, completion) } } @@ -598,26 +598,26 @@ public extension Transition { case update } - private let f: (Stage, UIView, CGPoint, Transition, @escaping () -> Void) -> Void + private let f: (Stage, UIView, CGPoint, ComponentTransition, @escaping () -> Void) -> Void - public init(_ f: @escaping (Stage, UIView, CGPoint, Transition, @escaping () -> Void) -> Void + public init(_ f: @escaping (Stage, UIView, CGPoint, ComponentTransition, @escaping () -> Void) -> Void ) { self.f = f } - public func callAsFunction(stage: Stage, view: UIView, guide: CGPoint, transition: Transition, completion: @escaping () -> Void) { + public func callAsFunction(stage: Stage, view: UIView, guide: CGPoint, transition: ComponentTransition, completion: @escaping () -> Void) { self.f(stage, view, guide, transition, completion) } } final class Update { - private let f: (_UpdatedChildComponent, UIView, Transition) -> Void + private let f: (_UpdatedChildComponent, UIView, ComponentTransition) -> Void - public init(_ f: @escaping (_UpdatedChildComponent, UIView, Transition) -> Void) { + public init(_ f: @escaping (_UpdatedChildComponent, UIView, ComponentTransition) -> Void) { self.f = f } - public func callAsFunction(component: _UpdatedChildComponent, view: UIView, transition: Transition) { + public func callAsFunction(component: _UpdatedChildComponent, view: UIView, transition: ComponentTransition) { self.f(component, view, transition) } } @@ -628,7 +628,7 @@ public extension CombinedComponent { return UIView() } - func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { let context = view.getCombinedComponentContext(Self.self) let storedBody: Body @@ -683,7 +683,7 @@ public extension CombinedComponent { previousView.transition = updatedChild.transitionDisappear previousView.transitionWithGuide = updatedChild.transitionDisappearWithGuide - (updatedChild.transitionUpdate ?? Transition.Update.default)(component: updatedChild, view: updatedChild.view, transition: transition) + (updatedChild.transitionUpdate ?? ComponentTransition.Update.default)(component: updatedChild, view: updatedChild.view, transition: transition) } else { for i in index ..< context.childViewIndices.count { if let moveView = context.childViews[context.childViewIndices[i]] { diff --git a/submodules/ComponentFlow/Source/Base/Component.swift b/submodules/ComponentFlow/Source/Base/Component.swift index fabce5cf5cc..69e0627fec2 100644 --- a/submodules/ComponentFlow/Source/Base/Component.swift +++ b/submodules/ComponentFlow/Source/Base/Component.swift @@ -89,13 +89,13 @@ extension UIView { } open class ComponentState { - open var _updated: ((Transition, Bool) -> Void)? + open var _updated: ((ComponentTransition, Bool) -> Void)? var isUpdated: Bool = false public init() { } - public final func updated(transition: Transition = .immediate, isLocal: Bool = false) { + public final func updated(transition: ComponentTransition = .immediate, isLocal: Bool = false) { self.isUpdated = true self._updated?(transition, isLocal) } @@ -107,7 +107,7 @@ public final class EmptyComponentState: ComponentState { public protocol _TypeErasedComponent { func _makeView() -> UIView func _makeContext() -> _TypeErasedComponentContext - func _update(view: UIView, availableSize: CGSize, environment: Any, transition: Transition) -> CGSize + func _update(view: UIView, availableSize: CGSize, environment: Any, transition: ComponentTransition) -> CGSize func _isEqual(to other: _TypeErasedComponent) -> Bool } @@ -127,7 +127,7 @@ public protocol Component: _TypeErasedComponent, Equatable { func makeView() -> View func makeState() -> State - func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize + func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize } public extension Component { @@ -139,7 +139,7 @@ public extension Component { return ComponentContext(component: self, environment: Environment(), state: self.makeState()) } - func _update(view: UIView, availableSize: CGSize, environment: Any, transition: Transition) -> CGSize { + func _update(view: UIView, availableSize: CGSize, environment: Any, transition: ComponentTransition) -> CGSize { let view = view as! Self.View return self.update(view: view, availableSize: availableSize, state: view.context(component: self).state, environment: environment as! Environment, transition: transition) @@ -191,7 +191,7 @@ public class AnyComponent: _TypeErasedComponent, Equatable { return self.wrapped._makeContext() } - public func _update(view: UIView, availableSize: CGSize, environment: Any, transition: Transition) -> CGSize { + public func _update(view: UIView, availableSize: CGSize, environment: Any, transition: ComponentTransition) -> CGSize { return self.wrapped._update(view: view, availableSize: availableSize, environment: environment as! Environment, transition: transition) } diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 6ed046685dc..227a314ad1b 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -17,7 +17,7 @@ public extension UIView { } private extension CALayer { - func animate(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, delay: Double, curve: Transition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) { + func animate(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, delay: Double, curve: ComponentTransition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) { let timingFunction: String let mediaTimingFunction: CAMediaTimingFunction? switch curve { @@ -44,7 +44,7 @@ private extension CALayer { } } -private extension Transition.Animation.Curve { +private extension ComponentTransition.Animation.Curve { func asTimingFunction() -> CAMediaTimingFunction { switch self { case .easeInOut: @@ -59,7 +59,7 @@ private extension Transition.Animation.Curve { } } -public extension Transition.Animation { +public extension ComponentTransition.Animation { var isImmediate: Bool { if case .none = self { return true @@ -69,7 +69,7 @@ public extension Transition.Animation { } } -public struct Transition { +public struct ComponentTransition { public enum Animation { public enum Curve { case easeInOut @@ -111,19 +111,19 @@ public struct Transition { return nil } - public func withUserData(_ userData: Any) -> Transition { + public func withUserData(_ userData: Any) -> ComponentTransition { var result = self result._userData.append(userData) return result } - public func withAnimation(_ animation: Animation) -> Transition { + public func withAnimation(_ animation: Animation) -> ComponentTransition { var result = self result.animation = animation return result } - public func withAnimationIfAnimated(_ animation: Animation) -> Transition { + public func withAnimationIfAnimated(_ animation: Animation) -> ComponentTransition { switch self.animation { case .none: return self @@ -134,14 +134,14 @@ public struct Transition { } } - public static var immediate: Transition = Transition(animation: .none) + public static var immediate: ComponentTransition = ComponentTransition(animation: .none) - public static func easeInOut(duration: Double) -> Transition { - return Transition(animation: .curve(duration: duration, curve: .easeInOut)) + public static func easeInOut(duration: Double) -> ComponentTransition { + return ComponentTransition(animation: .curve(duration: duration, curve: .easeInOut)) } - public static func spring(duration: Double) -> Transition { - return Transition(animation: .curve(duration: duration, curve: .spring)) + public static func spring(duration: Double) -> ComponentTransition { + return ComponentTransition(animation: .curve(duration: duration, curve: .spring)) } public init(animation: Animation) { @@ -1184,7 +1184,7 @@ public struct Transition { } } - public func animateContentsImage(layer: CALayer, from fromImage: CGImage, to toImage: CGImage, duration: Double, curve: Transition.Animation.Curve, completion: ((Bool) -> Void)? = nil) { + public func animateContentsImage(layer: CALayer, from fromImage: CGImage, to toImage: CGImage, duration: Double, curve: ComponentTransition.Animation.Curve, completion: ((Bool) -> Void)? = nil) { layer.animate( from: fromImage, to: toImage, diff --git a/submodules/ComponentFlow/Source/Components/Button.swift b/submodules/ComponentFlow/Source/Components/Button.swift index 41211e34ba8..c599f0ee6ca 100644 --- a/submodules/ComponentFlow/Source/Components/Button.swift +++ b/submodules/ComponentFlow/Source/Components/Button.swift @@ -154,7 +154,7 @@ public final class Button: Component { } } - private func updateAlpha(transition: Transition) { + private func updateAlpha(transition: ComponentTransition) { guard let component = self.component else { return } @@ -271,7 +271,7 @@ public final class Button: Component { super.cancelTracking(with: event) } - func update(component: Button, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: Button, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let contentSize = self.contentView.update( transition: transition, component: component.content, @@ -301,7 +301,7 @@ public final class Button: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/ComponentFlow/Source/Components/Circle.swift b/submodules/ComponentFlow/Source/Components/Circle.swift index cb770423f09..d999fec87f2 100644 --- a/submodules/ComponentFlow/Source/Components/Circle.swift +++ b/submodules/ComponentFlow/Source/Components/Circle.swift @@ -34,7 +34,7 @@ public final class Circle: Component { var component: Circle? var currentSize: CGSize? - func update(component: Circle, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: Circle, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let size = CGSize(width: min(availableSize.width, component.size.width), height: min(availableSize.height, component.size.height)) if self.currentSize != size || self.component != component { @@ -63,7 +63,7 @@ public final class Circle: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/ComponentFlow/Source/Components/Image.swift b/submodules/ComponentFlow/Source/Components/Image.swift index dfc4d34ecc2..78475c942f3 100644 --- a/submodules/ComponentFlow/Source/Components/Image.swift +++ b/submodules/ComponentFlow/Source/Components/Image.swift @@ -44,7 +44,7 @@ public final class Image: Component { preconditionFailure() } - func update(component: Image, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(component: Image, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { self.image = component.image self.contentMode = component.contentMode @@ -63,7 +63,7 @@ public final class Image: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } diff --git a/submodules/ComponentFlow/Source/Components/List.swift b/submodules/ComponentFlow/Source/Components/List.swift index 2b8601496fa..e3a29376e24 100644 --- a/submodules/ComponentFlow/Source/Components/List.swift +++ b/submodules/ComponentFlow/Source/Components/List.swift @@ -12,9 +12,9 @@ public final class List: CombinedComponent { private let items: [AnyComponentWithIdentity] private let direction: Direction private let centerAlignment: Bool - private let appear: Transition.Appear + private let appear: ComponentTransition.Appear - public init(_ items: [AnyComponentWithIdentity], direction: Direction = .vertical, centerAlignment: Bool = false, appear: Transition.Appear = .default()) { + public init(_ items: [AnyComponentWithIdentity], direction: Direction = .vertical, centerAlignment: Bool = false, appear: ComponentTransition.Appear = .default()) { self.items = items self.direction = direction self.centerAlignment = centerAlignment diff --git a/submodules/ComponentFlow/Source/Components/Rectangle.swift b/submodules/ComponentFlow/Source/Components/Rectangle.swift index 8eaf0971e4f..bed1e004b96 100644 --- a/submodules/ComponentFlow/Source/Components/Rectangle.swift +++ b/submodules/ComponentFlow/Source/Components/Rectangle.swift @@ -53,7 +53,7 @@ public final class Rectangle: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { var size = availableSize if let width = self.width { size.width = min(size.width, width) diff --git a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift index 275772854a1..2c6777b0767 100644 --- a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift +++ b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift @@ -47,7 +47,7 @@ public final class RoundedRectangle: Component { public final class View: UIImageView { var component: RoundedRectangle? - func update(component: RoundedRectangle, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: RoundedRectangle, availableSize: CGSize, transition: ComponentTransition) -> CGSize { if self.component != component { let cornerRadius = component.cornerRadius ?? min(availableSize.width, availableSize.height) * 0.5 @@ -113,7 +113,7 @@ public final class RoundedRectangle: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/ComponentFlow/Source/Components/Text.swift b/submodules/ComponentFlow/Source/Components/Text.swift index 394d59f19c1..b364de12249 100644 --- a/submodules/ComponentFlow/Source/Components/Text.swift +++ b/submodules/ComponentFlow/Source/Components/Text.swift @@ -95,7 +95,7 @@ public final class Text: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize) } } diff --git a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift index cdc4a7c3abd..ad99b99cf13 100644 --- a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift +++ b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift @@ -37,13 +37,13 @@ public final class ComponentHostView: UIView { fatalError("init(coder:) has not been implemented") } - public func update(transition: Transition, component: AnyComponent, @EnvironmentBuilder environment: () -> Environment, forceUpdate: Bool = false, containerSize: CGSize) -> CGSize { + public func update(transition: ComponentTransition, component: AnyComponent, @EnvironmentBuilder environment: () -> Environment, forceUpdate: Bool = false, containerSize: CGSize) -> CGSize { let size = self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, forceUpdate: forceUpdate, containerSize: containerSize) self.currentSize = size return size } - private func _update(transition: Transition, component: AnyComponent, maybeEnvironment: () -> Environment, updateEnvironment: Bool, forceUpdate: Bool, containerSize: CGSize) -> CGSize { + private func _update(transition: ComponentTransition, component: AnyComponent, maybeEnvironment: () -> Environment, updateEnvironment: Bool, forceUpdate: Bool, containerSize: CGSize) -> CGSize { precondition(!self.isUpdating) self.isUpdating = true @@ -150,13 +150,13 @@ public final class ComponentView { fatalError("init(coder:) has not been implemented") } - public func update(transition: Transition, component: AnyComponent, @EnvironmentBuilder environment: () -> Environment, forceUpdate: Bool = false, containerSize: CGSize) -> CGSize { + public func update(transition: ComponentTransition, component: AnyComponent, @EnvironmentBuilder environment: () -> Environment, forceUpdate: Bool = false, containerSize: CGSize) -> CGSize { let size = self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, forceUpdate: forceUpdate, containerSize: containerSize) self.currentSize = size return size } - public func updateEnvironment(transition: Transition, @EnvironmentBuilder environment: () -> Environment) -> CGSize? { + public func updateEnvironment(transition: ComponentTransition, @EnvironmentBuilder environment: () -> Environment) -> CGSize? { guard let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize else { return nil } @@ -165,7 +165,7 @@ public final class ComponentView { return size } - private func _update(transition: Transition, component: AnyComponent, maybeEnvironment: () -> Environment, updateEnvironment: Bool, forceUpdate: Bool, containerSize: CGSize) -> CGSize { + private func _update(transition: ComponentTransition, component: AnyComponent, maybeEnvironment: () -> Environment, updateEnvironment: Bool, forceUpdate: Bool, containerSize: CGSize) -> CGSize { precondition(!self.isUpdating) self.isUpdating = true diff --git a/submodules/Components/ActivityIndicatorComponent/Sources/ActivityIndicatorComponent.swift b/submodules/Components/ActivityIndicatorComponent/Sources/ActivityIndicatorComponent.swift index 27311afa4ab..263929fd428 100644 --- a/submodules/Components/ActivityIndicatorComponent/Sources/ActivityIndicatorComponent.swift +++ b/submodules/Components/ActivityIndicatorComponent/Sources/ActivityIndicatorComponent.swift @@ -27,7 +27,7 @@ public final class ActivityIndicatorComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ActivityIndicatorComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: ActivityIndicatorComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { if component.color != self.color { self.color = component.color } @@ -44,7 +44,7 @@ public final class ActivityIndicatorComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/Components/AnimatedStickerComponent/Sources/AnimatedStickerComponent.swift b/submodules/Components/AnimatedStickerComponent/Sources/AnimatedStickerComponent.swift index ef150762f15..84dd63706c9 100644 --- a/submodules/Components/AnimatedStickerComponent/Sources/AnimatedStickerComponent.swift +++ b/submodules/Components/AnimatedStickerComponent/Sources/AnimatedStickerComponent.swift @@ -91,7 +91,7 @@ public final class AnimatedStickerComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: AnimatedStickerComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: AnimatedStickerComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { if self.component?.animation != component.animation { self.animationNode?.view.removeFromSuperview() @@ -145,7 +145,7 @@ public final class AnimatedStickerComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift b/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift index 14802b61aa9..0d7dd997693 100644 --- a/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift +++ b/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift @@ -144,7 +144,7 @@ public final class BalancedTextComponent: Component { return self.textView.attributeSubstring(name: name, index: index) } - public func update(component: BalancedTextComponent, availableSize: CGSize, transition: Transition) -> CGSize { + public func update(component: BalancedTextComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let attributedString: NSAttributedString switch component.text { case let .plain(string): @@ -203,7 +203,7 @@ public final class BalancedTextComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/Components/BlurredBackgroundComponent/Sources/BlurredBackgroundComponent.swift b/submodules/Components/BlurredBackgroundComponent/Sources/BlurredBackgroundComponent.swift index 2b7d46cd69d..8849f4785f7 100644 --- a/submodules/Components/BlurredBackgroundComponent/Sources/BlurredBackgroundComponent.swift +++ b/submodules/Components/BlurredBackgroundComponent/Sources/BlurredBackgroundComponent.swift @@ -36,7 +36,7 @@ public final class BlurredBackgroundComponent: Component { private var tintContainerView: UIView? private var vibrancyEffectView: UIVisualEffectView? - public func update(component: BlurredBackgroundComponent, availableSize: CGSize, transition: Transition) -> CGSize { + public func update(component: BlurredBackgroundComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.updateColor(color: component.color, transition: transition.containedViewLayoutTransition) self.update(size: availableSize, cornerRadius: component.cornerRadius, transition: transition.containedViewLayoutTransition) @@ -56,7 +56,7 @@ public final class BlurredBackgroundComponent: Component { return View(color: nil, enableBlur: true) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift b/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift index e055375462e..d6165246afb 100644 --- a/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift +++ b/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift @@ -39,7 +39,7 @@ public final class BundleIconComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: BundleIconComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: BundleIconComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { if self.component?.name != component.name || self.component?.tintColor != component.tintColor { if let tintColor = component.tintColor { self.image = generateTintedImage(image: UIImage(bundleImageName: component.name), color: tintColor, backgroundColor: nil) @@ -62,7 +62,7 @@ public final class BundleIconComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift b/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift index 33adade5abd..942f79628f2 100644 --- a/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift +++ b/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift @@ -3,7 +3,7 @@ import UIKit import ComponentFlow import Display -public extension Transition.Animation.Curve { +public extension ComponentTransition.Animation.Curve { init(_ curve: ContainedViewLayoutTransitionCurve) { switch curve { case .linear: @@ -33,13 +33,13 @@ public extension Transition.Animation.Curve { } } -public extension Transition { +public extension ComponentTransition { init(_ transition: ContainedViewLayoutTransition) { switch transition { case .immediate: self.init(animation: .none) case let .animated(duration, curve): - self.init(animation: .curve(duration: duration, curve: Transition.Animation.Curve(curve))) + self.init(animation: .curve(duration: duration, curve: ComponentTransition.Animation.Curve(curve))) } } diff --git a/submodules/Components/Forms/CreditCardInputComponent/Sources/CreditCardInputComponent.swift b/submodules/Components/Forms/CreditCardInputComponent/Sources/CreditCardInputComponent.swift index cc36130746f..22a3613d8b1 100644 --- a/submodules/Components/Forms/CreditCardInputComponent/Sources/CreditCardInputComponent.swift +++ b/submodules/Components/Forms/CreditCardInputComponent/Sources/CreditCardInputComponent.swift @@ -133,7 +133,7 @@ public final class CreditCardInputComponent: Component { } } - func update(component: CreditCardInputComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: CreditCardInputComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { switch component.dataType { case .cardNumber: self.textField.autoFormattingBehavior = .cardNumbers @@ -166,7 +166,7 @@ public final class CreditCardInputComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/Components/Forms/PrefixSectionGroupComponent/Sources/PrefixSectionGroupComponent.swift b/submodules/Components/Forms/PrefixSectionGroupComponent/Sources/PrefixSectionGroupComponent.swift index f0c86926b17..b4e355a19e9 100644 --- a/submodules/Components/Forms/PrefixSectionGroupComponent/Sources/PrefixSectionGroupComponent.swift +++ b/submodules/Components/Forms/PrefixSectionGroupComponent/Sources/PrefixSectionGroupComponent.swift @@ -71,7 +71,7 @@ public final class PrefixSectionGroupComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: PrefixSectionGroupComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PrefixSectionGroupComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let spacing: CGFloat = 16.0 let sideInset: CGFloat = 16.0 @@ -188,7 +188,7 @@ public final class PrefixSectionGroupComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/Components/Forms/TextInputComponent/Sources/TextInputComponent.swift b/submodules/Components/Forms/TextInputComponent/Sources/TextInputComponent.swift index b1d14eacf4b..1e4f6adfbd5 100644 --- a/submodules/Components/Forms/TextInputComponent/Sources/TextInputComponent.swift +++ b/submodules/Components/Forms/TextInputComponent/Sources/TextInputComponent.swift @@ -58,7 +58,7 @@ public final class TextInputComponent: Component { self.component?.updated(self.text ?? "") } - func update(component: TextInputComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: TextInputComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.font = UIFont.systemFont(ofSize: 17.0) self.textColor = component.textColor @@ -80,7 +80,7 @@ public final class TextInputComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift index 03743f5b6b0..b13b4e4e928 100644 --- a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift +++ b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift @@ -146,7 +146,7 @@ public final class LottieAnimationComponent: Component { } } - func update(component: LottieAnimationComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: LottieAnimationComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { var updatePlayback = false var updateColors = false @@ -319,7 +319,7 @@ public final class LottieAnimationComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift index c6d8bd169bd..4a1f5886567 100644 --- a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift +++ b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift @@ -120,7 +120,7 @@ public final class MultilineTextComponent: Component { } public final class View: ImmediateTextView { - public func update(component: MultilineTextComponent, availableSize: CGSize, transition: Transition) -> CGSize { + public func update(component: MultilineTextComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let attributedString: NSAttributedString switch component.text { case let .plain(string): @@ -169,7 +169,7 @@ public final class MultilineTextComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift index 082666d28c4..a0f9198def5 100644 --- a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift +++ b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift @@ -145,7 +145,7 @@ public final class MultilineTextWithEntitiesComponent: Component { fatalError("init(coder:) has not been implemented") } - public func update(component: MultilineTextWithEntitiesComponent, availableSize: CGSize, transition: Transition) -> CGSize { + public func update(component: MultilineTextWithEntitiesComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let attributedString: NSAttributedString switch component.text { case let .plain(string): @@ -205,7 +205,7 @@ public final class MultilineTextWithEntitiesComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift index 7eb6f184d36..de0f925063d 100644 --- a/submodules/Components/PagerComponent/Sources/PagerComponent.swift +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -14,7 +14,7 @@ open class PagerExternalTopPanelContainer: SparseContainerView { } public protocol PagerContentViewWithBackground: UIView { - func pagerUpdateBackground(backgroundFrame: CGRect, topPanelHeight: CGFloat, transition: Transition) + func pagerUpdateBackground(backgroundFrame: CGRect, topPanelHeight: CGFloat, transition: ComponentTransition) } public final class PagerComponentChildEnvironment: Equatable { @@ -24,7 +24,7 @@ public final class PagerComponentChildEnvironment: Equatable { public var absoluteOffsetToBottomEdge: CGFloat? public var isReset: Bool public var isInteracting: Bool - public var transition: Transition + public var transition: ComponentTransition public init( relativeOffset: CGFloat, @@ -32,7 +32,7 @@ public final class PagerComponentChildEnvironment: Equatable { absoluteOffsetToBottomEdge: CGFloat?, isReset: Bool, isInteracting: Bool, - transition: Transition + transition: ComponentTransition ) { self.relativeOffset = relativeOffset self.absoluteOffsetToTopEdge = absoluteOffsetToTopEdge @@ -78,8 +78,8 @@ public final class PagerComponentPanelEnvironment: Equatabl public let contentAccessoryRightButtons: [AnyComponentWithIdentity] public let activeContentId: AnyHashable? public let navigateToContentId: (AnyHashable) -> Void - public let visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)> - public let isExpandedUpdated: (Bool, Transition) -> Void + public let visibilityFractionUpdated: ActionSlot<(CGFloat, ComponentTransition)> + public let isExpandedUpdated: (Bool, ComponentTransition) -> Void init( isContentInFocus: Bool, @@ -90,8 +90,8 @@ public final class PagerComponentPanelEnvironment: Equatabl contentAccessoryRightButtons: [AnyComponentWithIdentity], activeContentId: AnyHashable?, navigateToContentId: @escaping (AnyHashable) -> Void, - visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)>, - isExpandedUpdated: @escaping (Bool, Transition) -> Void + visibilityFractionUpdated: ActionSlot<(CGFloat, ComponentTransition)>, + isExpandedUpdated: @escaping (Bool, ComponentTransition) -> Void ) { self.isContentInFocus = isContentInFocus self.contentOffset = contentOffset @@ -206,9 +206,9 @@ public final class PagerComponent>? public let externalBottomPanelContainer: PagerExternalTopPanelContainer? - public let panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)? - public let isTopPanelExpandedUpdated: (Bool, Transition) -> Void - public let isTopPanelHiddenUpdated: (Bool, Transition) -> Void + public let panelStateUpdated: ((PagerComponentPanelState, ComponentTransition) -> Void)? + public let isTopPanelExpandedUpdated: (Bool, ComponentTransition) -> Void + public let isTopPanelHiddenUpdated: (Bool, ComponentTransition) -> Void public let contentIdUpdated: (AnyHashable) -> Void public let panelHideBehavior: PagerComponentPanelHideBehavior public let clipContentToTopPanel: Bool @@ -228,9 +228,9 @@ public final class PagerComponent>?, externalBottomPanelContainer: PagerExternalTopPanelContainer?, - panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)?, - isTopPanelExpandedUpdated: @escaping (Bool, Transition) -> Void, - isTopPanelHiddenUpdated: @escaping (Bool, Transition) -> Void, + panelStateUpdated: ((PagerComponentPanelState, ComponentTransition) -> Void)?, + isTopPanelExpandedUpdated: @escaping (Bool, ComponentTransition) -> Void, + isTopPanelHiddenUpdated: @escaping (Bool, ComponentTransition) -> Void, contentIdUpdated: @escaping (AnyHashable) -> Void, panelHideBehavior: PagerComponentPanelHideBehavior, clipContentToTopPanel: Bool, @@ -329,9 +329,9 @@ public final class PagerComponent? - private let topPanelVisibilityFractionUpdated = ActionSlot<(CGFloat, Transition)>() + private let topPanelVisibilityFractionUpdated = ActionSlot<(CGFloat, ComponentTransition)>() private var topPanelView: ComponentHostView>? - private let bottomPanelVisibilityFractionUpdated = ActionSlot<(CGFloat, Transition)>() + private let bottomPanelVisibilityFractionUpdated = ActionSlot<(CGFloat, ComponentTransition)>() private var bottomPanelView: ComponentHostView>? private var topPanelHeight: CGFloat? @@ -440,9 +440,9 @@ public final class PagerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PagerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousPanelHideBehavior = self.component?.panelHideBehavior var panelStateTransition = transition if let previousPanelHideBehavior = previousPanelHideBehavior, previousPanelHideBehavior != component.panelHideBehavior, panelStateTransition.animation.isImmediate { - panelStateTransition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + panelStateTransition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) } self.component = component @@ -1005,7 +1005,7 @@ public final class PagerComponent, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/Components/ProgressIndicatorComponent/Sources/ProgressIndicatorComponent.swift b/submodules/Components/ProgressIndicatorComponent/Sources/ProgressIndicatorComponent.swift index e16c9811300..fe9bea837c5 100644 --- a/submodules/Components/ProgressIndicatorComponent/Sources/ProgressIndicatorComponent.swift +++ b/submodules/Components/ProgressIndicatorComponent/Sources/ProgressIndicatorComponent.swift @@ -67,7 +67,7 @@ public final class ProgressIndicatorComponent: Component { return CAShapeLayer.self } - func update(component: ProgressIndicatorComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: ProgressIndicatorComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let lineWidth: CGFloat = 1.33 let size = CGSize(width: component.diameter, height: component.diameter) @@ -107,7 +107,7 @@ public final class ProgressIndicatorComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index bf81f82aa3f..7d4154d9101 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -20,6 +20,25 @@ private let tagImage: UIImage? = { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ReactionTagBackground"), color: .white)?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 15) }() +private final class StarsButtonEffectLayer: SimpleLayer { + override init() { + super.init() + + self.backgroundColor = UIColor.lightGray.withAlphaComponent(0.2).cgColor + } + + override init(layer: Any) { + super.init(layer: layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(size: CGSize) { + } +} + public final class ReactionIconView: PortalSourceView { private var animationLayer: InlineStickerItemLayer? @@ -705,14 +724,27 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { } } - let backgroundColors = ReactionButtonAsyncNode.ContainerButtonNode.Colors( - background: spec.component.chosenOrder != nil ? spec.component.colors.selectedBackground : spec.component.colors.deselectedBackground, - foreground: spec.component.chosenOrder != nil ? spec.component.colors.selectedForeground : spec.component.colors.deselectedForeground, - extractedBackground: spec.component.colors.extractedBackground, - extractedForeground: spec.component.colors.extractedForeground, - extractedSelectedForeground: spec.component.colors.extractedSelectedForeground, - isSelected: spec.component.chosenOrder != nil - ) + let backgroundColors: ReactionButtonAsyncNode.ContainerButtonNode.Colors + + if case .custom(MessageReaction.starsReactionId) = spec.component.reaction.value { + backgroundColors = ReactionButtonAsyncNode.ContainerButtonNode.Colors( + background: spec.component.chosenOrder != nil ? spec.component.colors.selectedStarsBackground : spec.component.colors.deselectedStarsBackground, + foreground: spec.component.chosenOrder != nil ? spec.component.colors.selectedStarsForeground : spec.component.colors.deselectedStarsForeground, + extractedBackground: spec.component.chosenOrder != nil ? spec.component.colors.selectedStarsBackground : spec.component.colors.deselectedStarsBackground, + extractedForeground: spec.component.chosenOrder != nil ? spec.component.colors.selectedStarsForeground : spec.component.colors.deselectedStarsForeground, + extractedSelectedForeground: spec.component.colors.extractedSelectedForeground, + isSelected: spec.component.chosenOrder != nil + ) + } else { + backgroundColors = ReactionButtonAsyncNode.ContainerButtonNode.Colors( + background: spec.component.chosenOrder != nil ? spec.component.colors.selectedBackground : spec.component.colors.deselectedBackground, + foreground: spec.component.chosenOrder != nil ? spec.component.colors.selectedForeground : spec.component.colors.deselectedForeground, + extractedBackground: spec.component.colors.extractedBackground, + extractedForeground: spec.component.colors.extractedForeground, + extractedSelectedForeground: spec.component.colors.extractedSelectedForeground, + isSelected: spec.component.chosenOrder != nil + ) + } var backgroundCounter: ReactionButtonAsyncNode.ContainerButtonNode.Counter? if let counterLayout = counterLayout { backgroundCounter = ReactionButtonAsyncNode.ContainerButtonNode.Counter( @@ -743,6 +775,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { public let containerView: ContextExtractedContentContainingView private let buttonNode: ContainerButtonNode + private var starsEffectLayer: StarsButtonEffectLayer? public var iconView: ReactionIconView? private var avatarsView: AnimatedAvatarSetView? @@ -838,6 +871,29 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { self.containerView.contentRect = CGRect(origin: CGPoint(), size: layout.size) animation.animator.updateFrame(layer: self.buttonNode.layer, frame: CGRect(origin: CGPoint(), size: layout.size), completion: nil) + if case .custom(MessageReaction.starsReactionId) = layout.spec.component.reaction.value { + let starsEffectLayer: StarsButtonEffectLayer + if let current = self.starsEffectLayer { + starsEffectLayer = current + } else { + starsEffectLayer = StarsButtonEffectLayer() + self.starsEffectLayer = starsEffectLayer + if let iconView = self.iconView { + self.buttonNode.layer.insertSublayer(starsEffectLayer, below: iconView.layer) + } else { + self.buttonNode.layer.insertSublayer(starsEffectLayer, at: 0) + } + } + let starsEffectLayerFrame = CGRect(origin: CGPoint(), size: layout.size) + animation.animator.updateFrame(layer: starsEffectLayer, frame: starsEffectLayerFrame, completion: nil) + starsEffectLayer.update(size: starsEffectLayerFrame.size) + } else { + if let starsEffectLayer = self.starsEffectLayer { + self.starsEffectLayer = nil + starsEffectLayer.removeFromSuperlayer() + } + } + self.buttonNode.update(layout: layout.backgroundLayout) if let iconView = self.iconView { @@ -878,65 +934,6 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { transition: animation.transition ) } - - /*if self.layout?.spec.component.reaction != layout.spec.component.reaction { - if let file = layout.spec.component.reaction.centerAnimation { - - if let image = ReactionImageCache.shared.get(reaction: layout.spec.component.reaction.value) { - iconView.imageView.image = image - } else { - self.iconImageDisposable.set((reactionStaticImage(context: layout.spec.component.context, animation: file, pixelSize: CGSize(width: 32.0 * UIScreenScale, height: 32.0 * UIScreenScale), queue: sharedReactionStaticImage) - |> filter { data in - return data.isComplete - } - |> take(1) - |> map { data -> UIImage? in - if data.isComplete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { - if let image = UIImage(data: dataValue) { - return image.precomposed() - } else { - print("Could not decode image") - } - } else { - print("Incomplete data") - } - return nil - } - |> deliverOnMainQueue).start(next: { [weak self] image in - guard let strongSelf = self else { - return - } - - if let image = image { - strongSelf.iconView?.imageView.image = image - ReactionImageCache.shared.put(reaction: layout.spec.component.reaction.value, image: image) - } - })) - } - } else if let legacyIcon = layout.spec.component.reaction.legacyIcon { - self.iconImageDisposable.set((layout.spec.component.context.account.postbox.mediaBox.resourceData(legacyIcon.resource) - |> deliverOn(Queue.concurrentDefaultQueue()) - |> map { data -> UIImage? in - if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { - if let image = WebP.convert(fromWebP: dataValue) { - if #available(iOS 15.0, iOSApplicationExtension 15.0, *) { - return image.preparingForDisplay() - } else { - return image.precomposed() - } - } - } - return nil - } - |> deliverOnMainQueue).start(next: { [weak self] image in - guard let strongSelf = self else { - return - } - - strongSelf.iconView?.imageView.image = image - })) - } - }*/ } if !layout.spec.component.avatarPeers.isEmpty { @@ -1041,6 +1038,10 @@ public final class ReactionButtonComponent: Equatable { public var selectedBackground: UInt32 public var deselectedForeground: UInt32 public var selectedForeground: UInt32 + public var deselectedStarsBackground: UInt32 + public var selectedStarsBackground: UInt32 + public var deselectedStarsForeground: UInt32 + public var selectedStarsForeground: UInt32 public var extractedBackground: UInt32 public var extractedForeground: UInt32 public var extractedSelectedForeground: UInt32 @@ -1052,6 +1053,10 @@ public final class ReactionButtonComponent: Equatable { selectedBackground: UInt32, deselectedForeground: UInt32, selectedForeground: UInt32, + deselectedStarsBackground: UInt32, + selectedStarsBackground: UInt32, + deselectedStarsForeground: UInt32, + selectedStarsForeground: UInt32, extractedBackground: UInt32, extractedForeground: UInt32, extractedSelectedForeground: UInt32, @@ -1062,6 +1067,10 @@ public final class ReactionButtonComponent: Equatable { self.selectedBackground = selectedBackground self.deselectedForeground = deselectedForeground self.selectedForeground = selectedForeground + self.deselectedStarsBackground = deselectedStarsBackground + self.selectedStarsBackground = selectedStarsBackground + self.deselectedStarsForeground = deselectedStarsForeground + self.selectedStarsForeground = selectedStarsForeground self.extractedBackground = extractedBackground self.extractedForeground = extractedForeground self.extractedSelectedForeground = extractedSelectedForeground @@ -1243,8 +1252,7 @@ public final class ReactionButtonsAsyncLayoutContainer { var items: [Result.Item] = [] var applyItems: [(key: MessageReaction.Reaction, size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation, _ arguments: Arguments) -> ReactionNodePool.Item)] = [] - var validIds = Set() - for reaction in reactions.sorted(by: { lhs, rhs in + var reactions = reactions.sorted(by: { lhs, rhs in var lhsCount = lhs.count if lhs.chosenOrder != nil { lhsCount -= 1 @@ -1268,7 +1276,22 @@ public final class ReactionButtonsAsyncLayoutContainer { } return false + }) + + if let index = reactions.firstIndex(where: { + if case .custom(MessageReaction.starsReactionId) = $0.reaction.value { + return true + } else { + return false + } }) { + let value = reactions[index] + reactions.remove(at: index) + reactions.insert(value, at: 0) + } + + var validIds = Set() + for reaction in reactions { validIds.insert(reaction.reaction.value) var avatarPeers = reaction.peers diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index d623afc5800..259baf3c123 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -391,6 +391,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent private final class ItemNode: HighlightTrackingButtonNode { let context: AccountContext let displayReadTimestamps: Bool + let displayReactionIcon: Bool let availableReactions: AvailableReactions? let animationCache: AnimationCache let animationRenderer: MultiAnimationRenderer @@ -411,10 +412,11 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent private var item: EngineMessageReactionListContext.Item? - init(context: AccountContext, displayReadTimestamps: Bool, availableReactions: AvailableReactions?, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, action: @escaping () -> Void) { + init(context: AccountContext, displayReadTimestamps: Bool, displayReactionIcon: Bool, availableReactions: AvailableReactions?, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, action: @escaping () -> Void) { self.action = action self.context = context self.displayReadTimestamps = displayReadTimestamps + self.displayReactionIcon = displayReactionIcon self.availableReactions = availableReactions self.animationCache = animationCache self.animationRenderer = animationRenderer @@ -548,7 +550,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let reaction: MessageReaction.Reaction? = item.reaction - if reaction != self.item?.reaction { + if self.displayReactionIcon, reaction != self.item?.reaction { if let reaction = reaction { switch reaction { case .builtin: @@ -802,6 +804,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent private let context: AccountContext private let displayReadTimestamps: Bool + private let displayReactionIcons: Bool private let availableReactions: AvailableReactions? private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer @@ -833,6 +836,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent init( context: AccountContext, displayReadTimestamps: Bool, + displayReactionIcons: Bool, availableReactions: AvailableReactions?, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, @@ -845,6 +849,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent ) { self.context = context self.displayReadTimestamps = displayReadTimestamps + self.displayReactionIcons = displayReactionIcons self.availableReactions = availableReactions self.animationCache = animationCache self.animationRenderer = animationRenderer @@ -955,7 +960,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } else { let openPeer = self.openPeer let peer = item.peer - itemNode = ItemNode(context: self.context, displayReadTimestamps: self.displayReadTimestamps, availableReactions: self.availableReactions, animationCache: self.animationCache, animationRenderer: self.animationRenderer, action: { + itemNode = ItemNode(context: self.context, displayReadTimestamps: self.displayReadTimestamps, displayReactionIcon: self.displayReactionIcons, availableReactions: self.availableReactions, animationCache: self.animationCache, animationRenderer: self.animationRenderer, action: { openPeer(peer, item.reaction != nil) }) self.itemNodes[index] = itemNode @@ -1104,6 +1109,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent final class ItemsNode: ASDisplayNode, ContextControllerItemsNode, ASGestureRecognizerDelegate { private let context: AccountContext private let displayReadTimestamps: Bool + private let displayReactionIcons: Bool private let availableReactions: AvailableReactions? private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer @@ -1148,6 +1154,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent ) { self.context = context self.displayReadTimestamps = displayReadTimestamps + self.displayReactionIcons = reaction == nil self.availableReactions = availableReactions self.animationCache = animationCache self.animationRenderer = animationRenderer @@ -1159,9 +1166,6 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.requestUpdate = requestUpdate self.requestUpdateApparentHeight = requestUpdateApparentHeight - //var requestUpdateTab: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)? - //var requestUpdateTabApparentHeight: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)? - if let back = back { self.backButtonNode = BackButtonNode() self.backButtonNode?.action = { @@ -1218,45 +1222,9 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent strongSelf.tabListNode?.scrollToTabReaction = ReactionTabListNode.ScrollToTabReaction(value: reaction) strongSelf.currentTabIndex = tabIndex - /*let currentTabNode = ReactionsTabNode( - context: context, - availableReactions: availableReactions, - message: message, - reaction: reaction, - readStats: nil, - requestUpdate: { tab, transition in - requestUpdateTab?(tab, transition) - }, - requestUpdateApparentHeight: { tab, transition in - requestUpdateTabApparentHeight?(tab, transition) - }, - openPeer: { id in - openPeer(id) - } - ) - strongSelf.currentTabNode = currentTabNode - strongSelf.addSubnode(currentTabNode)*/ strongSelf.requestUpdate(.animated(duration: 0.45, curve: .spring)) } - /*requestUpdateTab = { [weak self] tab, transition in - guard let strongSelf = self else { - return - } - if strongSelf.visibleTabNodes.contains(where: { $0.value === tab }) { - strongSelf.requestUpdate(transition) - } - } - - requestUpdateTabApparentHeight = { [weak self] tab, transition in - guard let strongSelf = self else { - return - } - if strongSelf.visibleTabNodes.contains(where: { $0.value === tab }) { - strongSelf.requestUpdateApparentHeight(transition) - } - }*/ - let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in guard let strongSelf = self else { return [] @@ -1371,6 +1339,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent tabNode = ReactionsTabNode( context: self.context, displayReadTimestamps: self.displayReadTimestamps, + displayReactionIcons: self.displayReactionIcons, availableReactions: self.availableReactions, animationCache: self.animationCache, animationRenderer: self.animationRenderer, diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index b3bf62bc1e9..39124655941 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -63,6 +63,7 @@ public final class SheetComponent: Component { public let backgroundColor: BackgroundColor public let followContentSizeChanges: Bool public let clipsContent: Bool + public let isScrollEnabled: Bool public let externalState: ExternalState? public let animateOut: ActionSlot> public let onPan: () -> Void @@ -72,6 +73,7 @@ public final class SheetComponent: Component { backgroundColor: BackgroundColor, followContentSizeChanges: Bool = false, clipsContent: Bool = false, + isScrollEnabled: Bool = true, externalState: ExternalState? = nil, animateOut: ActionSlot>, onPan: @escaping () -> Void = {} @@ -80,6 +82,7 @@ public final class SheetComponent: Component { self.backgroundColor = backgroundColor self.followContentSizeChanges = followContentSizeChanges self.clipsContent = clipsContent + self.isScrollEnabled = isScrollEnabled self.externalState = externalState self.animateOut = animateOut self.onPan = onPan @@ -299,7 +302,7 @@ public final class SheetComponent: Component { private var currentHasInputHeight = false private var currentAvailableSize: CGSize? - func update(component: SheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: SheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousHasInputHeight = self.currentHasInputHeight let sheetEnvironment = environment[SheetComponentEnvironment.self].value component.animateOut.connect { [weak self] completion in @@ -371,7 +374,7 @@ public final class SheetComponent: Component { transition.setFrame(view: effectView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) } } else { - transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: contentSize), completion: nil) + transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 100.0)), completion: nil) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) if let effectView = self.effectView { transition.setFrame(view: effectView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) @@ -389,6 +392,8 @@ public final class SheetComponent: Component { updateContentSize() } + self.scrollView.isScrollEnabled = component.isScrollEnabled + self.ignoreScrolling = false if let currentAvailableSize = self.currentAvailableSize, currentAvailableSize.height != availableSize.height { self.scrollView.contentOffset = CGPoint(x: 0.0, y: -(availableSize.height - contentSize.height)) @@ -419,7 +424,7 @@ public final class SheetComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift b/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift index 9bfdef86f62..aa65028e56f 100644 --- a/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift +++ b/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift @@ -122,7 +122,7 @@ public final class SolidRoundedButtonComponent: Component { private var currentIsLoading = false - public func update(component: SolidRoundedButtonComponent, availableSize: CGSize, transition: Transition) -> CGSize { + public func update(component: SolidRoundedButtonComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { if self.button == nil { let button = SolidRoundedButtonView( title: component.title, @@ -182,7 +182,7 @@ public final class SolidRoundedButtonComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/Components/UndoPanelComponent/Sources/UndoPanelComponent.swift b/submodules/Components/UndoPanelComponent/Sources/UndoPanelComponent.swift index e662d0f3c54..7a2b5beab27 100644 --- a/submodules/Components/UndoPanelComponent/Sources/UndoPanelComponent.swift +++ b/submodules/Components/UndoPanelComponent/Sources/UndoPanelComponent.swift @@ -48,7 +48,7 @@ public final class UndoPanelComponent: Component { fatalError("init(coder:) has not been implemented") } - public func update(component: UndoPanelComponent, availableSize: CGSize, transition: Transition) -> CGSize { + public func update(component: UndoPanelComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.effect = UIBlurEffect(style: .dark) self.layer.cornerRadius = 10.0 @@ -61,7 +61,7 @@ public final class UndoPanelComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/Components/UndoPanelComponent/Sources/UndoPanelContainerComponent.swift b/submodules/Components/UndoPanelComponent/Sources/UndoPanelContainerComponent.swift index ec7eb73d5ca..d14fe8ffdc4 100644 --- a/submodules/Components/UndoPanelComponent/Sources/UndoPanelContainerComponent.swift +++ b/submodules/Components/UndoPanelComponent/Sources/UndoPanelContainerComponent.swift @@ -22,14 +22,14 @@ public final class UndoPanelContainerComponent: Component { private var nextPanel: UndoPanelComponent? - public func update(component: UndoPanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, transition: Transition) -> CGSize { + public func update(component: UndoPanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize { component.push.connect { [weak self, weak state] panel in guard let strongSelf = self, let state = state else { return } strongSelf.nextPanel = panel - state.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + state.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } var animateTopPanelIn = false @@ -77,7 +77,7 @@ public final class UndoPanelContainerComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, transition: transition) } } diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index d86b99280ac..cc205210cc8 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -166,7 +166,7 @@ open class ViewControllerComponentContainer: ViewController { self.view.addSubview(self.hostView) } - func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) { + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ComponentTransition) { self.currentLayout = (layout, navigationHeight) let environment = ViewControllerComponentContainer.Environment( @@ -206,10 +206,10 @@ open class ViewControllerComponentContainer: ViewController { guard let currentLayout = self.currentLayout else { return } - self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: animated ? Transition(animation: .none).withUserData(isVisible ? AnimateInTransition() : AnimateOutTransition()) : .immediate) + self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: animated ? ComponentTransition(animation: .none).withUserData(isVisible ? AnimateInTransition() : AnimateOutTransition()) : .immediate) } - func updateComponent(component: AnyComponent, transition: Transition) { + func updateComponent(component: AnyComponent, transition: ComponentTransition) { self.component = component guard let currentLayout = self.currentLayout else { @@ -369,10 +369,10 @@ open class ViewControllerComponentContainer: ViewController { let navigationHeight = self.navigationLayout(layout: layout).navigationFrame.maxY self.validLayout = layout - self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) } - public func updateComponent(component: AnyComponent, transition: Transition) { + public func updateComponent(component: AnyComponent, transition: ComponentTransition) { self.node.updateComponent(component: component, transition: transition) } } diff --git a/submodules/ComposePollUI/Sources/ComposePollScreen.swift b/submodules/ComposePollUI/Sources/ComposePollScreen.swift index f8366a68d09..526009e4c6e 100644 --- a/submodules/ComposePollUI/Sources/ComposePollScreen.swift +++ b/submodules/ComposePollUI/Sources/ComposePollScreen.swift @@ -266,7 +266,7 @@ final class ComposePollScreenComponent: Component { } } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { let navigationAlphaDistance: CGFloat = 16.0 let navigationAlpha: CGFloat = max(0.0, min(1.0, self.scrollView.contentOffset.y / navigationAlphaDistance)) if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { @@ -297,7 +297,7 @@ final class ComposePollScreenComponent: Component { effectiveInputHeight: CGFloat, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, - transition: Transition + transition: ComponentTransition ) -> CGFloat { let bottomInset: CGFloat = bottomInset + 8.0 let bottomContainerInset: CGFloat = 0.0 @@ -377,8 +377,8 @@ final class ComposePollScreenComponent: Component { if needsInputActivation { let inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight) - Transition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) - Transition.immediate.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) + ComponentTransition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + ComponentTransition.immediate.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) } if animateIn { @@ -472,7 +472,7 @@ final class ComposePollScreenComponent: Component { return textInputStates } - func update(component: ComposePollScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ComposePollScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -622,7 +622,7 @@ final class ComposePollScreenComponent: Component { return } if !self.isUpdating { - self.state?.updated(transition: Transition(transition)) + self.state?.updated(transition: ComponentTransition(transition)) } } ) @@ -1458,7 +1458,7 @@ final class ComposePollScreenComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift b/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift index 42998dbb3a1..4aba30339b4 100644 --- a/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift +++ b/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift @@ -173,12 +173,12 @@ public final class ListComposePollOptionComponent: Component { self.layer.removeAnimation(forKey: "transform.scale") if animateScale { - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) transition.setScale(layer: self.layer, scale: topScale) } } else { if animateScale { - let transition = Transition(animation: .none) + let transition = ComponentTransition(animation: .none) transition.setScale(layer: self.layer, scale: 1.0) self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in @@ -202,7 +202,7 @@ public final class ListComposePollOptionComponent: Component { self.action?() } - func update(size: CGSize, theme: PresentationTheme, isSelected: Bool, transition: Transition) { + func update(size: CGSize, theme: PresentationTheme, isSelected: Bool, transition: ComponentTransition) { let checkLayer: CheckLayer if let current = self.checkLayer { checkLayer = current @@ -291,7 +291,7 @@ public final class ListComposePollOptionComponent: Component { } } - func update(component: ListComposePollOptionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ListComposePollOptionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -455,12 +455,12 @@ public final class ListComposePollOptionComponent: Component { ) let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - 4.0 - modeSelectorSize.width, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize) if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View { - let alphaTransition: Transition = .easeInOut(duration: 0.2) + let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) if modeSelectorView.superview == nil { self.addSubview(modeSelectorView) - Transition.immediate.setAlpha(view: modeSelectorView, alpha: 0.0) - Transition.immediate.setScale(view: modeSelectorView, scale: 0.001) + ComponentTransition.immediate.setAlpha(view: modeSelectorView, alpha: 0.0) + ComponentTransition.immediate.setScale(view: modeSelectorView, scale: 0.001) } if playAnimation, let animationView = modeSelectorView.contentView as? LottieComponent.View { @@ -481,7 +481,7 @@ public final class ListComposePollOptionComponent: Component { self.modeSelector = nil if let modeSelectorView = modeSelector.view { if !transition.animation.isImmediate { - let alphaTransition: Transition = .easeInOut(duration: 0.2) + let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) alphaTransition.setAlpha(view: modeSelectorView, alpha: 0.0, completion: { [weak modeSelectorView] _ in modeSelectorView?.removeFromSuperview() }) @@ -497,7 +497,7 @@ public final class ListComposePollOptionComponent: Component { return size } - public func updateCustomPlaceholder(value: String, size: CGSize, transition: Transition) { + public func updateCustomPlaceholder(value: String, size: CGSize, transition: ComponentTransition) { guard let component = self.component else { return } @@ -554,7 +554,7 @@ public final class ListComposePollOptionComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index be109ead0cd..82dde2f1fdc 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -4,6 +4,7 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore +import Postbox import TelegramPresentationData import TelegramUIPreferences import DeviceAccess @@ -379,7 +380,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } } -private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set, peerRequiresPremiumForMessaging: [EnginePeer.Id: Bool], authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool, storySubscriptions: EngineStorySubscriptions?, topPeers: [EnginePeer], topPeersPresentation: ContactListPresentation.TopPeers, interaction: ContactListNodeInteraction) -> [ContactListNodeEntry] { +private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set, peerRequiresPremiumForMessaging: [EnginePeer.Id: Bool], peersWithStories: [EnginePeer.Id: PeerStoryStats], authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool, storySubscriptions: EngineStorySubscriptions?, topPeers: [EnginePeer], topPeersPresentation: ContactListPresentation.TopPeers, interaction: ContactListNodeInteraction) -> [ContactListNodeEntry] { var entries: [ContactListNodeEntry] = [] var commonHeader: ListViewItemHeader? @@ -665,7 +666,9 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis } let presence = presences[peer.id] - entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false)) + entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, peersWithStories[peer.id].flatMap { + ContactListNodeEntry.StoryData(count: $0.totalCount, unseenCount: $0.unseenCount, hasUnseenCloseFriends: $0.hasUnseenCloseFriends) + }, false)) index += 1 } @@ -709,7 +712,14 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis enabled = true } - entries.append(.peer(index, peer, presence, nil, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, false, enabled, nil, false)) + var storyData: ContactListNodeEntry.StoryData? + if case let .peer(id) = peer.id { + storyData = peersWithStories[id].flatMap { + ContactListNodeEntry.StoryData(count: $0.totalCount, unseenCount: $0.unseenCount, hasUnseenCloseFriends: $0.hasUnseenCloseFriends) + } + } + + entries.append(.peer(index, peer, presence, nil, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, false, enabled, storyData, false)) index += 1 } } @@ -755,8 +765,14 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis enabled = true } + var storyData: ContactListNodeEntry.StoryData? + if case let .peer(id) = peer.id { + storyData = peersWithStories[id].flatMap { + ContactListNodeEntry.StoryData(count: $0.totalCount, unseenCount: $0.unseenCount, hasUnseenCloseFriends: $0.hasUnseenCloseFriends) + } + } - entries.append(.peer(index, peer, presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, false, enabled, nil, requiresPremiumForMessaging)) + entries.append(.peer(index, peer, presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, false, enabled, storyData, requiresPremiumForMessaging)) index += 1 } return entries @@ -927,7 +943,7 @@ public final class ContactListNode: ASDisplayNode { } private var didSetReady = false - private let contactPeersViewPromise = Promise<(EngineContactList, EnginePeer?, [EnginePeer.Id: Bool])>() + private let contactPeersViewPromise = Promise<(EngineContactList, EnginePeer?, [EnginePeer.Id: Bool], [EnginePeer.Id: PeerStoryStats])>() let storySubscriptions = Promise(nil) private let selectionStatePromise = Promise(nil) @@ -1009,6 +1025,34 @@ public final class ContactListNode: ASDisplayNode { } else { contactsWithPremiumRequired = .single([:]) } + + let contactsWithStories: Signal<[EnginePeer.Id: PeerStoryStats], NoError> = self.context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Contacts.List(includePresences: false) + ) + |> map { contacts -> Set in + var result = Set() + for peer in contacts.peers { + result.insert(peer.id) + } + return result + } + |> distinctUntilChanged + |> mapToSignal { peerIds -> Signal<[EnginePeer.Id: PeerStoryStats], NoError> in + return context.engine.data.subscribe( + EngineDataMap( + peerIds.map(TelegramEngine.EngineData.Item.Peer.StoryStats.init(id:)) + ) + ) + |> map { result -> [EnginePeer.Id: PeerStoryStats] in + var filtered: [EnginePeer.Id: PeerStoryStats] = [:] + for (id, value) in result { + if let value { + filtered[id] = value + } + } + return filtered + } + } if value { self.contactPeersViewPromise.set(combineLatest( @@ -1016,10 +1060,11 @@ public final class ContactListNode: ASDisplayNode { TelegramEngine.EngineData.Item.Contacts.List(includePresences: true), TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.engine.account.peerId) ), - contactsWithPremiumRequired + contactsWithPremiumRequired, + contactsWithStories ) - |> mapToThrottled { next, contactsWithPremiumRequired -> Signal<(EngineContactList, EnginePeer?, [EnginePeer.Id: Bool]), NoError> in - return .single((next.0, next.1, contactsWithPremiumRequired)) + |> mapToThrottled { next, contactsWithPremiumRequired, contactsWithStories -> Signal<(EngineContactList, EnginePeer?, [EnginePeer.Id: Bool], [EnginePeer.Id: PeerStoryStats]), NoError> in + return .single((next.0, next.1, contactsWithPremiumRequired, contactsWithStories)) |> then( .complete() |> delay(5.0, queue: Queue.concurrentDefaultQueue()) @@ -1030,9 +1075,9 @@ public final class ContactListNode: ASDisplayNode { TelegramEngine.EngineData.Item.Contacts.List(includePresences: true), TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.engine.account.peerId) ), - contactsWithPremiumRequired) - |> map { next, contactsWithPremiumRequired -> (EngineContactList, EnginePeer?, [EnginePeer.Id: Bool]) in - return (next.0, next.1, contactsWithPremiumRequired) + contactsWithPremiumRequired, contactsWithStories) + |> map { next, contactsWithPremiumRequired, contactsWithStories -> (EngineContactList, EnginePeer?, [EnginePeer.Id: Bool], [EnginePeer.Id: PeerStoryStats]) in + return (next.0, next.1, contactsWithPremiumRequired, contactsWithStories) } |> take(1)) } @@ -1575,7 +1620,7 @@ public final class ContactListNode: ASDisplayNode { peers.append(.deviceContact(stableId, contact.0)) } - let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, peerRequiresPremiumForMessaging: peerRequiresPremiumForMessaging, authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons, storySubscriptions: nil, topPeers: [], topPeersPresentation: .none, interaction: interaction) + let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, peerRequiresPremiumForMessaging: peerRequiresPremiumForMessaging, peersWithStories: [:], authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons, storySubscriptions: nil, topPeers: [], topPeersPresentation: .none, interaction: interaction) let previous = previousEntries.swap(entries) return .single(preparedContactListNodeTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: false, generateIndexSections: generateSections, animation: .none, isSearch: isSearch)) } @@ -1772,7 +1817,7 @@ public final class ContactListNode: ASDisplayNode { isEmpty = true } - let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, peerRequiresPremiumForMessaging: view.2, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons, storySubscriptions: storySubscriptions, topPeers: topPeers.map { $0.peer }, topPeersPresentation: displayTopPeers, interaction: interaction) + let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, peerRequiresPremiumForMessaging: view.2, peersWithStories: view.3, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons, storySubscriptions: storySubscriptions, topPeers: topPeers.map { $0.peer }, topPeersPresentation: displayTopPeers, interaction: interaction) let previous = previousEntries.swap(entries) let previousSelection = previousSelectionState.swap(selectionState) let previousPendingRemovalPeerIds = previousPendingRemovalPeerIds.swap(pendingRemovalPeerIds) diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index a56ad6c0481..7f2fd359c9b 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -354,7 +354,7 @@ final class ContactsControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { ) let navigationBarSize = self.navigationBarView.update( - transition: Transition(transition), + transition: ComponentTransition(transition), component: AnyComponent(ChatListNavigationBar( context: self.context, theme: self.presentationData.theme, @@ -420,7 +420,7 @@ final class ContactsControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { } if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { - navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: false, transition: Transition(transition)) + navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: false, transition: ComponentTransition(transition)) } } @@ -450,7 +450,7 @@ final class ContactsControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { navigationBarComponentView.deferScrollApplication = false - navigationBarComponentView.applyCurrentScroll(transition: Transition(transition)) + navigationBarComponentView.applyCurrentScroll(transition: ComponentTransition(transition)) } } @@ -465,7 +465,7 @@ final class ContactsControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { let controller = ContextController(presentationData: self.presentationData, source: .extracted(ContactContextExtractedContentSource(sourceNode: node, shouldBeDismissed: .single(false))), items: items, recognizer: nil, gesture: gesture) contactsController.presentInGlobalOverlay(controller) } else { - let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing)) + let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) let contextController = ContextController(presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: items, gesture: gesture) contactsController.presentInGlobalOverlay(contextController) diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 4fb300ce218..663fca5d618 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -1131,7 +1131,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { lineWidth: 1.33, inactiveLineWidth: 1.33 ), - transition: animated ? Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) : .immediate + transition: animated ? ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)) : .immediate ) if strongSelf.avatarTapRecognizer == nil { diff --git a/submodules/ContextUI/BUILD b/submodules/ContextUI/BUILD index 661391694ce..e427db1de60 100644 --- a/submodules/ContextUI/BUILD +++ b/submodules/ContextUI/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/UIKitRuntimeUtils", + "//submodules/TelegramUI/Components/EmojiStatusComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift index 592efe01aee..ee65de32c0d 100644 --- a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift +++ b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift @@ -436,6 +436,12 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode { self.targetSelectionIndex = nil icon = nil isUserInteractionEnabled = action != nil + case let .starsReactions(topCount): + self.action = nil + self.text = "Send \(topCount) or more to highlight your profile" + self.targetSelectionIndex = nil + icon = nil + isUserInteractionEnabled = action != nil } self.iconNode = ASImageNode() diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index acf2dcbb5f4..ea7827e4fda 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -2277,6 +2277,7 @@ public final class ContextController: ViewController, StandalonePresentableContr public var allPresetReactionsAreAvailable: Bool public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? public var disablePositionLock: Bool + public var previewReaction: TelegramMediaFile? public var tip: Tip? public var tipSignal: Signal? public var dismissed: (() -> Void)? @@ -2294,6 +2295,7 @@ public final class ContextController: ViewController, StandalonePresentableContr allPresetReactionsAreAvailable: Bool = false, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? = nil, disablePositionLock: Bool = false, + previewReaction: TelegramMediaFile? = nil, tip: Tip? = nil, tipSignal: Signal? = nil, dismissed: (() -> Void)? = nil @@ -2310,6 +2312,7 @@ public final class ContextController: ViewController, StandalonePresentableContr self.allPresetReactionsAreAvailable = allPresetReactionsAreAvailable self.getEmojiContent = getEmojiContent self.disablePositionLock = disablePositionLock + self.previewReaction = previewReaction self.tip = tip self.tipSignal = tipSignal self.dismissed = dismissed @@ -2327,6 +2330,7 @@ public final class ContextController: ViewController, StandalonePresentableContr self.allPresetReactionsAreAvailable = false self.getEmojiContent = nil self.disablePositionLock = false + self.previewReaction = nil self.tip = nil self.tipSignal = nil self.dismissed = nil @@ -2345,6 +2349,7 @@ public final class ContextController: ViewController, StandalonePresentableContr case messageCopyProtection(isChannel: Bool) case animatedEmoji(text: String?, arguments: TextNodeWithEntities.Arguments?, file: TelegramMediaFile?, action: (() -> Void)?) case notificationTopicExceptions(text: String, action: (() -> Void)?) + case starsReactions(topCount: Int) public static func ==(lhs: Tip, rhs: Tip) -> Bool { switch lhs { @@ -2390,6 +2395,12 @@ public final class ContextController: ViewController, StandalonePresentableContr } else { return false } + case let .starsReactions(topCount): + if case .starsReactions(topCount) = rhs { + return true + } else { + return false + } } } } diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 6666b5da871..df2715d88d7 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -59,6 +59,16 @@ public struct ContextControllerReactionItems { } } +public final class ContextControllerPreviewReaction { + public let context: AccountContext + public let file: TelegramMediaFile + + public init(context: AccountContext, file: TelegramMediaFile) { + self.context = context + self.file = file + } +} + public protocol ContextControllerActionsStackItem: AnyObject { func node( getController: @escaping () -> ContextControllerProtocol?, @@ -71,6 +81,7 @@ public protocol ContextControllerActionsStackItem: AnyObject { var tip: ContextController.Tip? { get } var tipSignal: Signal? { get } var reactionItems: ContextControllerReactionItems? { get } + var previewReaction: ContextControllerPreviewReaction? { get } var dismissed: (() -> Void)? { get } } @@ -935,6 +946,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio public let id: AnyHashable? public let items: [ContextMenuItem] public let reactionItems: ContextControllerReactionItems? + public let previewReaction: ContextControllerPreviewReaction? public let tip: ContextController.Tip? public let tipSignal: Signal? public let dismissed: (() -> Void)? @@ -943,6 +955,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio id: AnyHashable?, items: [ContextMenuItem], reactionItems: ContextControllerReactionItems?, + previewReaction: ContextControllerPreviewReaction?, tip: ContextController.Tip?, tipSignal: Signal?, dismissed: (() -> Void)? @@ -950,6 +963,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio self.id = id self.items = items self.reactionItems = reactionItems + self.previewReaction = previewReaction self.tip = tip self.tipSignal = tipSignal self.dismissed = dismissed @@ -1033,6 +1047,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta let id: AnyHashable? private let content: ContextControllerItemsContent let reactionItems: ContextControllerReactionItems? + let previewReaction: ContextControllerPreviewReaction? let tip: ContextController.Tip? let tipSignal: Signal? let dismissed: (() -> Void)? @@ -1041,6 +1056,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta id: AnyHashable?, content: ContextControllerItemsContent, reactionItems: ContextControllerReactionItems?, + previewReaction: ContextControllerPreviewReaction?, tip: ContextController.Tip?, tipSignal: Signal?, dismissed: (() -> Void)? @@ -1048,6 +1064,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta self.id = id self.content = content self.reactionItems = reactionItems + self.previewReaction = previewReaction self.tip = tip self.tipSignal = tipSignal self.dismissed = dismissed @@ -1083,13 +1100,17 @@ func makeContextControllerActionsStackItem(items: ContextController.Items) -> [C getEmojiContent: items.getEmojiContent ) } + var previewReaction: ContextControllerPreviewReaction? + if let context = items.context, let file = items.previewReaction { + previewReaction = ContextControllerPreviewReaction(context: context, file: file) + } switch items.content { case let .list(listItems): - return [ContextControllerActionsListStackItem(id: items.id, items: listItems, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)] + return [ContextControllerActionsListStackItem(id: items.id, items: listItems, reactionItems: reactionItems, previewReaction: previewReaction, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)] case let .twoLists(listItems1, listItems2): - return [ContextControllerActionsListStackItem(id: items.id, items: listItems1, reactionItems: nil, tip: nil, tipSignal: nil, dismissed: items.dismissed), ContextControllerActionsListStackItem(id: nil, items: listItems2, reactionItems: nil, tip: nil, tipSignal: nil, dismissed: nil)] + return [ContextControllerActionsListStackItem(id: items.id, items: listItems1, reactionItems: nil, previewReaction: nil, tip: nil, tipSignal: nil, dismissed: items.dismissed), ContextControllerActionsListStackItem(id: nil, items: listItems2, reactionItems: nil, previewReaction: nil, tip: nil, tipSignal: nil, dismissed: nil)] case let .custom(customContent): - return [ContextControllerActionsCustomStackItem(id: items.id, content: customContent, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)] + return [ContextControllerActionsCustomStackItem(id: items.id, content: customContent, reactionItems: reactionItems, previewReaction: previewReaction, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)] } } @@ -1206,6 +1227,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode { let tipSignal: Signal? var tipNode: InnerTextSelectionTipContainerNode? let reactionItems: ContextControllerReactionItems? + let previewReaction: ContextControllerPreviewReaction? let itemDismissed: (() -> Void)? var storedScrollingState: CGFloat? let positionLock: CGFloat? @@ -1221,6 +1243,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode { tip: ContextController.Tip?, tipSignal: Signal?, reactionItems: ContextControllerReactionItems?, + previewReaction: ContextControllerPreviewReaction?, itemDismissed: (() -> Void)?, positionLock: CGFloat? ) { @@ -1239,6 +1262,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode { self.dimNode.alpha = 0.0 self.reactionItems = reactionItems + self.previewReaction = previewReaction self.itemDismissed = itemDismissed self.positionLock = positionLock @@ -1375,6 +1399,10 @@ public final class ContextControllerActionsStackNode: ASDisplayNode { return self.itemContainers.last?.reactionItems } + public var topPreviewReaction: ContextControllerPreviewReaction? { + return self.itemContainers.last?.previewReaction + } + public var topPositionLock: CGFloat? { return self.itemContainers.last?.positionLock } @@ -1508,6 +1536,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode { tip: item.tip, tipSignal: item.tipSignal, reactionItems: item.reactionItems, + previewReaction: item.previewReaction, itemDismissed: item.dismissed, positionLock: positionLock ) diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 0faa57a5958..8e840e14f13 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -241,6 +241,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo private let scrollNode: ASDisplayNode private var reactionContextNode: ReactionContextNode? + private var reactionPreviewView: ReactionPreviewView? private var reactionContextNodeIsAnimatingOut: Bool = false private var itemContentNode: ItemContentNode? @@ -637,6 +638,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo var animateReactionsIn = false var contentTopInset: CGFloat = topInset var removedReactionContextNode: ReactionContextNode? + if let reactionItems = self.actionsStackNode.topReactionItems, !reactionItems.reactionItems.isEmpty { let reactionContextNode: ReactionContextNode if let current = self.reactionContextNode { @@ -733,6 +735,25 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo removedReactionContextNode = reactionContextNode } + let reactionPreviewSize = CGSize(width: 100.0, height: 100.0) + let reactionPreviewInset: CGFloat = 7.0 + var removedReactionPreviewView: ReactionPreviewView? + if self.reactionContextNode == nil, let previewReaction = self.actionsStackNode.topPreviewReaction { + let reactionPreviewView: ReactionPreviewView + if let current = self.reactionPreviewView { + reactionPreviewView = current + } else { + reactionPreviewView = ReactionPreviewView(context: previewReaction.context, file: previewReaction.file) + self.reactionPreviewView = reactionPreviewView + self.view.addSubview(reactionPreviewView) + } + + contentTopInset += reactionPreviewSize.height + reactionPreviewInset + } else { + removedReactionPreviewView = self.reactionPreviewView + self.reactionPreviewView = nil + } + if let contentNode = itemContentNode { switch stateTransition { case .animateIn, .animateOut: @@ -963,6 +984,14 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.proposedReactionsPositionLock = nil } + if let reactionPreviewView = self.reactionPreviewView { + let anchorRect = contentRect.offsetBy(dx: contentParentGlobalFrame.minX, dy: 0.0) + + let reactionPreviewFrame = CGRect(origin: CGPoint(x: floor((anchorRect.midX - reactionPreviewSize.width * 0.5)), y: anchorRect.minY - reactionPreviewInset - reactionPreviewSize.height), size: reactionPreviewSize) + transition.updateFrame(view: reactionPreviewView, frame: reactionPreviewFrame) + reactionPreviewView.update(size: reactionPreviewFrame.size) + } + if let _ = self.currentReactionsPositionLock { transition.updateAlpha(node: self.actionsStackNode, alpha: 0.0) } else { @@ -976,6 +1005,12 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo }) } + if let removedReactionPreviewView { + transition.updateAlpha(layer: removedReactionPreviewView.layer, alpha: 0.0, completion: { [weak removedReactionPreviewView] _ in + removedReactionPreviewView?.removeFromSuperview() + }) + } + transition.updateFrame(node: self.contentRectDebugNode, frame: contentRect, beginWithCurrentState: true) var actionsFrame: CGRect @@ -1066,6 +1101,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if case let .extracted(extracted) = self.source { if extracted.adjustContentHorizontally { contentFrame.origin.x = combinedActionsFrame.minX + if contentFrame.maxX > layout.size.width { + contentFrame.origin.x = layout.size.width - contentFrame.width - actionsEdgeInset + } } if extracted.centerVertically { if combinedActionsFrame.height.isZero { @@ -1214,6 +1252,29 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo damping: springDamping, additive: true ) + + if let reactionPreviewView = self.reactionPreviewView { + reactionPreviewView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + reactionPreviewView.layer.animateSpring( + from: -animationInContentYDistance as NSNumber, to: 0.0 as NSNumber, + keyPath: "position.y", + duration: duration, + delay: 0.0, + initialVelocity: 0.0, + damping: springDamping, + additive: true + ) + reactionPreviewView.layer.animateSpring( + from: 0.01 as NSNumber, + to: 1.0 as NSNumber, + keyPath: "transform.scale", + duration: duration, + delay: 0.0, + initialVelocity: 0.0, + damping: springDamping, + additive: false + ) + } } else if let contentNode = controllerContentNode { if case let .controller(source) = self.source, let transitionInfo = source.transitionInfo(), let (sourceView, sourceRect) = transitionInfo.sourceNode() { let sourcePoint = sourceView.convert(sourceRect.center, to: self.view) @@ -1502,6 +1563,32 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo }) } ) + + if let reactionPreviewView = self.reactionPreviewView { + reactionPreviewView.layer.animate( + from: 0.0 as NSNumber, + to: -animationInContentYDistance as NSNumber, + keyPath: "position.y", + timingFunction: timingFunction, + duration: duration, + delay: 0.0, + removeOnCompletion: true, + additive: true, + completion: { _ in + } + ) + reactionPreviewView.layer.animate( + from: 1.0 as NSNumber, + to: 0.01 as NSNumber, + keyPath: "transform.scale", + timingFunction: timingFunction, + duration: duration, + delay: 0.0, + removeOnCompletion: false, + additive: false + ) + reactionPreviewView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in }) + } } if let contentNode = controllerContentNode { if case let .controller(source) = self.source, let transitionInfo = source.transitionInfo(), let (sourceView, sourceRect) = transitionInfo.sourceNode() { diff --git a/submodules/ContextUI/Sources/ContextSourceContainer.swift b/submodules/ContextUI/Sources/ContextSourceContainer.swift index 9c41f964a26..ee940e2e490 100644 --- a/submodules/ContextUI/Sources/ContextSourceContainer.swift +++ b/submodules/ContextUI/Sources/ContextSourceContainer.swift @@ -635,7 +635,7 @@ final class ContextSourceContainer: ASDisplayNode { return TabSelectorComponent.Item(id: source.id, title: source.title) } let tabSelectorSize = tabSelector.update( - transition: Transition(transition), + transition: ComponentTransition(transition), component: AnyComponent(TabSelectorComponent( colors: TabSelectorComponent.Colors( foreground: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.8), @@ -678,7 +678,7 @@ final class ContextSourceContainer: ASDisplayNode { } let closeButtonSize = closeButton.update( - transition: Transition(transition), + transition: ComponentTransition(transition), component: AnyComponent(PlainButtonComponent( content: AnyComponent( CloseButtonComponent( diff --git a/submodules/ContextUI/Sources/ReactionPreviewView.swift b/submodules/ContextUI/Sources/ReactionPreviewView.swift new file mode 100644 index 00000000000..753087581c2 --- /dev/null +++ b/submodules/ContextUI/Sources/ReactionPreviewView.swift @@ -0,0 +1,55 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Display +import ComponentFlow +import TelegramCore +import AccountContext +import EmojiStatusComponent + +final class ReactionPreviewView: UIView { + private let context: AccountContext + private let file: TelegramMediaFile + + private let icon = ComponentView() + + init(context: AccountContext, file: TelegramMediaFile) { + self.context = context + self.file = file + + super.init(frame: CGRect()) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func update(size: CGSize) { + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.context.animationCache, + animationRenderer: self.context.animationRenderer, + content: .animation( + content: .file(file: self.file), + size: size, + placeholderColor: .clear, + themeColor: .white, + loopMode: .count(0) + ), + isVisibleForAnimations: true, + action: nil + )), + environment: {}, + containerSize: size + ) + let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) * 0.5), y: floor((size.height - iconSize.height) * 0.5)), size: iconSize) + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconView.frame = iconFrame + } + } +} diff --git a/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionController.swift b/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionController.swift index b29c7353aa8..186e7ded8ce 100644 --- a/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionController.swift +++ b/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionController.swift @@ -21,7 +21,7 @@ private func loadCountryCodes() -> [Country] { } let delimiter = ";" - let endOfLine = "\n" + let endOfLine = "\r\n" var result: [Country] = [] var countriesByPrefix: [String: (Country, Country.CountryCode)] = [:] diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 199cc78cc32..8bc7ec79d10 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -113,7 +113,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case playlistPlayback(Bool) case enableQuickReactionSwitch(Bool) case disableReloginTokens(Bool) - case voiceConference + case liveStreamV2(Bool) case preferredVideoCodec(Int, String, String?, Bool) case disableVideoAspectScaling(Bool) case enableNetworkFramework(Bool) @@ -143,7 +143,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.web.rawValue case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: return DebugControllerSection.experiments.rawValue - case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens: + case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2: return DebugControllerSection.experiments.rawValue case .logTranslationRecognition, .resetTranslationStates: return DebugControllerSection.translation.rawValue @@ -262,7 +262,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 49 case .enableQuickReactionSwitch: return 50 - case .voiceConference: + case .liveStreamV2: return 51 case let .preferredVideoCodec(index, _, _, _): return 52 + index @@ -1367,11 +1367,15 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) - case .voiceConference: - return ItemListDisclosureItem(presentationData: presentationData, title: "Voice Conference (Test)", label: "", sectionId: self.section, style: .blocks, action: { - guard let _ = arguments.context else { - return - } + case let .liveStreamV2(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Live Stream V2", value: value, sectionId: self.section, style: .blocks, updated: { value in + let _ = arguments.sharedContext.accountManager.transaction ({ transaction in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in + var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings + settings.liveStreamV2 = value + return PreferencesEntry(settings) + }) + }).start() }) case let .preferredVideoCodec(_, title, value, isSelected): return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .right, checked: isSelected, zeroSeparatorInsets: false, sectionId: self.section, action: { @@ -1526,9 +1530,13 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.experimentalCompatibility(experimentalSettings.experimentalCompatibility)) entries.append(.enableDebugDataDisplay(experimentalSettings.enableDebugDataDisplay)) entries.append(.acceleratedStickers(experimentalSettings.acceleratedStickers)) + #if DEBUG + entries.append(.browserExperiment(experimentalSettings.browserExperiment)) + #else if sharedContext.applicationBindings.appBuildType == .internal { entries.append(.browserExperiment(experimentalSettings.browserExperiment)) } + #endif entries.append(.localTranscription(experimentalSettings.localTranscription)) if case .internal = sharedContext.applicationBindings.appBuildType { entries.append(.enableReactionOverrides(experimentalSettings.enableReactionOverrides)) @@ -1545,6 +1553,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present } entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction)) + entries.append(.liveStreamV2(experimentalSettings.liveStreamV2)) } let codecs: [(String, String?)] = [ @@ -1562,7 +1571,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present if isMainApp { entries.append(.disableVideoAspectScaling(experimentalSettings.disableVideoAspectScaling)) entries.append(.enableNetworkFramework(networkSettings?.useNetworkFramework ?? useBetaFeatures)) - entries.append(.enableNetworkExperiments(networkSettings?.useExperimentalDownload ?? false)) + entries.append(.enableNetworkExperiments(networkSettings?.useExperimentalDownload ?? true)) } if let backupHostOverride = networkSettings?.backupHostOverride { diff --git a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift index 6336580241f..7f6756ad146 100644 --- a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift +++ b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift @@ -54,7 +54,7 @@ public final class DeviceLocationManager: NSObject { self.manager.delegate = self self.manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters - self.manager.distanceFilter = 5.0 +// self.manager.distanceFilter = 5.0 self.manager.activityType = .other self.manager.pausesLocationUpdatesAutomatically = false self.manager.headingFilter = 2.0 diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index c74db47d293..eea065a5f12 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -553,18 +553,18 @@ public extension ContainedViewLayoutTransition { } } - func animateFrame(layer: CALayer, from frame: CGRect, to toFrame: CGRect? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + func animateFrame(layer: CALayer, from frame: CGRect, to toFrame: CGRect? = nil, delay: Double = 0.0, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self { - case .immediate: + case .immediate: + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + layer.animateFrame(from: frame, to: toFrame ?? layer.frame, duration: duration, delay: delay, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { result in if let completion = completion { - completion(true) + completion(result) } - case let .animated(duration, curve): - layer.animateFrame(from: frame, to: toFrame ?? layer.frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { result in - if let completion = completion { - completion(result) - } - }) + }) } } @@ -1177,9 +1177,11 @@ public extension ContainedViewLayoutTransition { self.updateTransform(layer: node.layer, transform: transform, beginWithCurrentState: beginWithCurrentState, delay: delay, completion: completion) } - func updateTransform(layer: CALayer, transform: CGAffineTransform, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { - let transform = CATransform3DMakeAffineTransform(transform) - + func updateTransform(node: ASDisplayNode, transform: CATransform3D, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + self.updateTransform(layer: node.layer, transform: transform, beginWithCurrentState: beginWithCurrentState, delay: delay, completion: completion) + } + + func updateTransform(layer: CALayer, transform: CATransform3D, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { if CATransform3DEqualToTransform(layer.transform, transform) { if let completion = completion { completion(true) @@ -1206,6 +1208,11 @@ public extension ContainedViewLayoutTransition { }) } } + + func updateTransform(layer: CALayer, transform: CGAffineTransform, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + let transform = CATransform3DMakeAffineTransform(transform) + self.updateTransform(layer: layer, transform: transform, beginWithCurrentState: beginWithCurrentState, delay: delay, completion: completion) + } func updateTransformScale(node: ASDisplayNode, scale: CGFloat, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { let t = node.layer.transform @@ -1990,12 +1997,6 @@ extension CGRect: AnyValueProviding { } } -extension CATransform3D: Equatable { - public static func ==(lhs: CATransform3D, rhs: CATransform3D) -> Bool { - return CATransform3DEqualToTransform(lhs, rhs) - } -} - extension CATransform3D: AnyValueProviding { func interpolate(with other: CATransform3D, fraction: CGFloat) -> CATransform3D { return CATransform3D( @@ -2025,7 +2026,7 @@ extension CATransform3D: AnyValueProviding { stringValue: { "\(self)" }, isEqual: { other in if let otherValue = other.value as? CATransform3D { - return self == otherValue + return CATransform3DEqualToTransform(self, otherValue) } else { return false } @@ -2112,7 +2113,7 @@ final class ControlledTransitionProperty { return "MyCustomAnimation_\(Unmanaged.passUnretained(self).toOpaque())" }() - init(layer: CALayer, path: String, fromValue: T, toValue: T, completion: ((Bool) -> Void)?) where T: AnyValueProviding { + init(layer: CALayer, path: String, fromValue: T, toValue: T, completion: ((Bool) -> Void)?) where T: AnyValueProviding { self.layer = layer self.path = path self.fromValue = fromValue.anyValue @@ -2333,7 +2334,7 @@ public final class ControlledTransition { } public func updateTransform(layer: CALayer, transform: CATransform3D, completion: ((Bool) -> Void)?) { - if layer.transform == transform { + if CATransform3DEqualToTransform(layer.transform, transform) { return } let fromValue: CATransform3D diff --git a/submodules/Display/Source/ContextMenuAction.swift b/submodules/Display/Source/ContextMenuAction.swift index 37a766860b8..3660673cfd5 100644 --- a/submodules/Display/Source/ContextMenuAction.swift +++ b/submodules/Display/Source/ContextMenuAction.swift @@ -4,6 +4,7 @@ public enum ContextMenuActionContent { case text(title: String, accessibilityLabel: String) case icon(UIImage) case textWithIcon(title: String, icon: UIImage?) + case textWithSubtitleAndIcon(title: String, subtitle: String, icon: UIImage?) } public struct ContextMenuAction { diff --git a/submodules/Display/Source/KeyShortcut.swift b/submodules/Display/Source/KeyShortcut.swift index a0883b547cd..8997ee43b1e 100644 --- a/submodules/Display/Source/KeyShortcut.swift +++ b/submodules/Display/Source/KeyShortcut.swift @@ -15,7 +15,7 @@ public struct KeyShortcut: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(self.input) - hasher.combine(self.modifiers) + hasher.combine(self.modifiers.rawValue) } public static func ==(lhs: KeyShortcut, rhs: KeyShortcut) -> Bool { @@ -23,12 +23,6 @@ public struct KeyShortcut: Hashable { } } -extension UIKeyModifierFlags: Hashable { - public var hashValue: Int { - return self.rawValue - } -} - extension KeyShortcut { var uiKeyCommand: UIKeyCommand { let command = UIKeyCommand(input: self.input, modifierFlags: self.modifiers, action: #selector(KeyShortcutsController.handleKeyCommand(_:)), discoverabilityTitle: self.title) diff --git a/submodules/Display/Source/Navigation/MinimizedContainer.swift b/submodules/Display/Source/Navigation/MinimizedContainer.swift new file mode 100644 index 00000000000..b8b5b2b1fac --- /dev/null +++ b/submodules/Display/Source/Navigation/MinimizedContainer.swift @@ -0,0 +1,18 @@ +import Foundation +import AsyncDisplayKit + +public protocol MinimizedContainer: ASDisplayNode { + var navigationController: NavigationController? { get set } + var controllers: [ViewController] { get } + var isExpanded: Bool { get } + + var willMaximize: (() -> Void)? { get set } + + func addController(_ viewController: ViewController, beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void, transition: ContainedViewLayoutTransition) + func maximizeController(_ viewController: ViewController, animated: Bool, completion: @escaping (Bool) -> Void) + func collapse() + func dismissAll(completion: @escaping () -> Void) + + func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) + func collapsedHeight(layout: ContainerViewLayout) -> CGFloat +} diff --git a/submodules/Display/Source/Navigation/NavigationContainer.swift b/submodules/Display/Source/Navigation/NavigationContainer.swift index e13399c474f..5fcb4946cc6 100644 --- a/submodules/Display/Source/Navigation/NavigationContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationContainer.swift @@ -77,6 +77,8 @@ public final class NavigationContainer: ASDisplayNode, ASGestureRecognizerDelega public private(set) var controllers: [ViewController] = [] private var state: State = State(layout: nil, canBeClosed: nil, top: nil, transition: nil, pending: nil) + weak var minimizedContainer: MinimizedContainer? + private var ignoreInputHeight: Bool = false public private(set) var isReady: Bool = false @@ -240,6 +242,13 @@ public final class NavigationContainer: ASDisplayNode, ASGestureRecognizerDelega let topNode = topController.displayNode var bottomControllerLayout = layout if bottomController.view.disableAutomaticKeyboardHandling.isEmpty { + if let minimizedContainer = self.minimizedContainer, (bottomControllerLayout.inputHeight ?? 0.0) > 0.0 { + var updatedSize = bottomControllerLayout.size + var updatedIntrinsicInsets = bottomControllerLayout.intrinsicInsets + updatedSize.height -= minimizedContainer.collapsedHeight(layout: layout) + updatedIntrinsicInsets.bottom = 0.0 + bottomControllerLayout = bottomControllerLayout.withUpdatedSize(updatedSize).withUpdatedIntrinsicInsets(updatedIntrinsicInsets) + } bottomControllerLayout = bottomControllerLayout.withUpdatedInputHeight(nil) } bottomController.containerLayoutUpdated(bottomControllerLayout, transition: .immediate) diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index 365152e8259..45d9f2f3002 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -150,6 +150,18 @@ open class NavigationController: UINavigationController, ContainableController, private var rootModalFrame: NavigationModalFrame? private var modalContainers: [NavigationModalContainer] = [] private var overlayContainers: [NavigationOverlayContainer] = [] + open var minimizedContainer: MinimizedContainer? { + didSet { + self.minimizedContainer?.navigationController = self + self.minimizedContainer?.willMaximize = { [weak self] in + guard let self else { + return + } + self.isMaximizing = true + self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring)) + } + } + } private var globalOverlayContainers: [NavigationOverlayContainer] = [] private var globalOverlayBelowKeyboardContainerParent: GlobalOverlayContainerParent? @@ -734,7 +746,7 @@ open class NavigationController: UINavigationController, ContainableController, modalStyleOverlayTransitionFactor = max(modalStyleOverlayTransitionFactor, lastController.modalStyleOverlayTransitionFactor) topFlatModalHasProgress = modalStyleOverlayTransitionFactor > 0.0 } - + containerTransition.updateFrame(node: modalContainer, frame: CGRect(origin: CGPoint(), size: layout.size)) modalContainer.update(layout: modalContainer.isFlat ? globalOverlayLayout : layout, controllers: navigationLayout.modal[i].controllers, coveredByModalTransition: effectiveModalTransition, transition: containerTransition) @@ -817,9 +829,34 @@ open class NavigationController: UINavigationController, ContainableController, } } + if self.isMaximizing && layout.size.width < layout.size.height { + modalStyleOverlayTransitionFactor = 1.0 + topFlatModalHasProgress = true + } + layout.additionalInsets.left = max(layout.intrinsicInsets.left, additionalSideInsets.left) layout.additionalInsets.right = max(layout.intrinsicInsets.right, additionalSideInsets.right) + var updatedSize = layout.size + var updatedIntrinsicInsets = layout.intrinsicInsets + if case .flat = navigationLayout.root, let minimizedContainer = self.minimizedContainer { + if minimizedContainer.supernode !== self.displayNode { + if let rootContainer = self.rootContainer, case let .flat(flatContainer) = rootContainer { + if let rootModalFrame = self.rootModalFrame { + self.displayNode.insertSubnode(minimizedContainer, aboveSubnode: rootModalFrame) + } else { + self.displayNode.insertSubnode(minimizedContainer, aboveSubnode: flatContainer) + } + } else { + self.displayNode.insertSubnode(minimizedContainer, at: 0) + } + } + if (layout.inputHeight ?? 0.0).isZero { + updatedSize.height -= minimizedContainer.collapsedHeight(layout: layout) + updatedIntrinsicInsets.bottom = 0.0 + } + } + switch navigationLayout.root { case let .flat(controllers): if let rootContainer = self.rootContainer { @@ -832,8 +869,11 @@ open class NavigationController: UINavigationController, ContainableController, flatContainer.keyboardViewManager = nil flatContainer.canHaveKeyboardFocus = false } - transition.updateFrame(node: flatContainer, frame: CGRect(origin: CGPoint(), size: layout.size)) - flatContainer.update(layout: layout, canBeClosed: false, controllers: controllers, transition: transition) + + let updatedLayout = layout.withUpdatedSize(updatedSize).withUpdatedIntrinsicInsets(updatedIntrinsicInsets) + transition.updateFrame(node: flatContainer, frame: CGRect(origin: CGPoint(), size: updatedSize)) + flatContainer.update(layout: updatedLayout, canBeClosed: false, controllers: controllers, transition: transition) + flatContainer.minimizedContainer = self.minimizedContainer case let .split(splitContainer): let flatContainer = NavigationContainer(isFlat: self.isFlat, controllerRemoved: { [weak self] controller in self?.controllerRemoved(controller) @@ -890,8 +930,10 @@ open class NavigationController: UINavigationController, ContainableController, self.displayNode.insertSubnode(flatContainer, at: 0) } self.rootContainer = .flat(flatContainer) - flatContainer.frame = CGRect(origin: CGPoint(), size: layout.size) - flatContainer.update(layout: layout, canBeClosed: false, controllers: controllers, transition: .immediate) + + let updatedLayout = layout.withUpdatedSize(updatedSize).withUpdatedIntrinsicInsets(updatedIntrinsicInsets) + flatContainer.frame = CGRect(origin: CGPoint(), size: updatedSize) + flatContainer.update(layout: updatedLayout, canBeClosed: false, controllers: controllers, transition: .immediate) } case let .split(masterControllers, detailControllers): if let rootContainer = self.rootContainer { @@ -917,6 +959,11 @@ open class NavigationController: UINavigationController, ContainableController, splitContainer.update(layout: layout, masterControllers: masterControllers, detailControllers: detailControllers, detailsPlaceholderNode: self.detailsPlaceholderNode, transition: .immediate) flatContainer.statusBarStyleUpdated = nil flatContainer.removeFromSupernode() + + if let minimizedContainer = self.minimizedContainer { + minimizedContainer.removeFromSupernode() + self.minimizedContainer = nil + } case let .split(splitContainer): if previousModalContainer == nil { splitContainer.canHaveKeyboardFocus = true @@ -1097,6 +1144,11 @@ open class NavigationController: UINavigationController, ContainableController, } } + if let minimizedContainer = self.minimizedContainer { + minimizedContainer.frame = CGRect(origin: .zero, size: layout.size) + minimizedContainer.updateLayout(layout, transition: transition) + } + if self.inCallStatusBar != nil { statusBarStyle = .White } @@ -1288,7 +1340,9 @@ open class NavigationController: UINavigationController, ContainableController, if let _ = self.inCallStatusBar { self.inCallNavigate?() } else if let rootContainer = self.rootContainer { - if let modalContainer = self.modalContainers.last { + if let minimizedContainer = self.minimizedContainer, minimizedContainer.isExpanded { + minimizedContainer.collapse() + } else if let modalContainer = self.modalContainers.last { modalContainer.container.controllers.last?.scrollToTop?() } else { switch rootContainer { @@ -1522,6 +1576,69 @@ open class NavigationController: UINavigationController, ContainableController, } self._viewControllersPromise.set(self.viewControllers) } + + public func minimizeViewController(_ viewController: ViewController, damping: CGFloat?, velocity: CGFloat? = nil, beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void, setupContainer: (MinimizedContainer?) -> MinimizedContainer?, animated: Bool) { + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .customSpring(damping: damping ?? 124.0, initialVelocity: velocity ?? 0.0)) : .immediate + + let minimizedContainer = setupContainer(self.minimizedContainer) + if self.minimizedContainer !== minimizedContainer { + minimizedContainer?.willMaximize = { [weak self] in + guard let self else { + return + } + self.isMaximizing = true + self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring)) + } + + self.minimizedContainer?.removeFromSupernode() + self.minimizedContainer = minimizedContainer + + self.updateContainersNonReentrant(transition: transition) + } + viewController.isMinimized = true + self.filterController(viewController, animated: true) + minimizedContainer?.addController(viewController, beforeMaximize: beforeMaximize, transition: transition) + } + + private var isMaximizing = false + public func maximizeViewController(_ viewController: ViewController, animated: Bool) { + guard let minimizedContainer = self.minimizedContainer else { + return + } + if animated { + self.isMaximizing = true + self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring)) + } + minimizedContainer.maximizeController(viewController, animated: animated, completion: { [weak self] dismissed in + guard let self else { + return + } + var viewControllers = self.viewControllers + viewControllers.append(viewController) + self.setViewControllers(viewControllers, animated: false) + + viewController.isMinimized = false + + self.isMaximizing = false + + if dismissed, let minimizedContainer = self.minimizedContainer { + self.minimizedContainer = nil + minimizedContainer.removeFromSupernode() + } + }) + } + + public func dismissMinimizedControllers(animated: Bool) { + guard let minimizedContainer = self.minimizedContainer else { + return + } + self.minimizedContainer = nil + + minimizedContainer.dismissAll(completion: { [weak minimizedContainer] in + minimizedContainer?.removeFromSupernode() + }) + self.updateContainersNonReentrant(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate) + } public var _keepModalDismissProgress = false public func presentOverlay(controller: ViewController, inGlobal: Bool = false, blockInteraction: Bool = false) { @@ -1786,5 +1903,9 @@ open class NavigationController: UINavigationController, ContainableController, return } transition.updateTransform(node: container, transform: CGAffineTransformMakeTranslation(offset, 0.0)) + + if let minimizedContainer = self.minimizedContainer { + transition.updateTransform(node: minimizedContainer, transform: CGAffineTransformMakeTranslation(offset, 0.0)) + } } } diff --git a/submodules/Display/Source/Navigation/NavigationLayout.swift b/submodules/Display/Source/Navigation/NavigationLayout.swift index d31ac9f6c16..839c4f41ea4 100644 --- a/submodules/Display/Source/Navigation/NavigationLayout.swift +++ b/submodules/Display/Source/Navigation/NavigationLayout.swift @@ -73,6 +73,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL rootControllers.append(controller) } } + let rootLayout: RootNavigationLayout switch mode { case .single: diff --git a/submodules/Display/Source/Navigation/NavigationModalContainer.swift b/submodules/Display/Source/Navigation/NavigationModalContainer.swift index 1e7603dcbf1..65128341fa7 100644 --- a/submodules/Display/Source/Navigation/NavigationModalContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationModalContainer.swift @@ -485,6 +485,9 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) alphaTransition.updateAlpha(node: self.dim, alpha: 0.0, beginWithCurrentState: true) + if let lastController = self.container.controllers.last, lastController.isMinimized { + self.dim.layer.removeAllAnimations() + } positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: self.bounds.height + self.container.bounds.height / 2.0 + self.bounds.height), beginWithCurrentState: true, completion: { [weak self] _ in guard let strongSelf = self else { return diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 4e71fb78ab2..b33274d4806 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -64,17 +64,19 @@ public final class NavigationBarTheme { public let disabledButtonColor: UIColor public let primaryTextColor: UIColor public let backgroundColor: UIColor + public let opaqueBackgroundColor: UIColor public let enableBackgroundBlur: Bool public let separatorColor: UIColor public let badgeBackgroundColor: UIColor public let badgeStrokeColor: UIColor public let badgeTextColor: UIColor - public init(buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, backgroundColor: UIColor, enableBackgroundBlur: Bool, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor) { + public init(buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, backgroundColor: UIColor, opaqueBackgroundColor: UIColor? = nil, enableBackgroundBlur: Bool, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor) { self.buttonColor = buttonColor self.disabledButtonColor = disabledButtonColor self.primaryTextColor = primaryTextColor self.backgroundColor = backgroundColor + self.opaqueBackgroundColor = opaqueBackgroundColor ?? backgroundColor self.enableBackgroundBlur = enableBackgroundBlur self.separatorColor = separatorColor self.badgeBackgroundColor = badgeBackgroundColor @@ -83,11 +85,11 @@ public final class NavigationBarTheme { } public func withUpdatedBackgroundColor(_ color: UIColor) -> NavigationBarTheme { - return NavigationBarTheme(buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: color, enableBackgroundBlur: false, separatorColor: self.separatorColor, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor) + return NavigationBarTheme(buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: color, opaqueBackgroundColor: self.opaqueBackgroundColor, enableBackgroundBlur: false, separatorColor: self.separatorColor, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor) } public func withUpdatedSeparatorColor(_ color: UIColor) -> NavigationBarTheme { - return NavigationBarTheme(buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: self.backgroundColor, enableBackgroundBlur: self.enableBackgroundBlur, separatorColor: color, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor) + return NavigationBarTheme(buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: self.backgroundColor, opaqueBackgroundColor: self.opaqueBackgroundColor, enableBackgroundBlur: self.enableBackgroundBlur, separatorColor: color, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor) } } diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index c2783805c46..4ed52f8c825 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -230,6 +230,10 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { private var navigationBarOrigin: CGFloat = 0.0 + public var minimizedTopEdgeOffset: CGFloat? + public var minimizedBounds: CGRect? + open var isMinimized: Bool = false + open var interactiveNavivationGestureEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth? { return nil } @@ -349,6 +353,23 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { } } + public var titleSignal: Signal { + return Signal { [weak self] subscriber in + guard let self else { + return EmptyDisposable + } + subscriber.putNext(self.navigationItem.title) + let listenerIndex = self.navigationItem.addSetTitleListener { title, _ in + subscriber.putNext(title) + } + return ActionDisposable { [weak self] in + if let self { + self.navigationItem.removeSetTitleListener(listenerIndex) + } + } + } + } + public init(navigationBarPresentationData: NavigationBarPresentationData?) { self.statusBar = StatusBar() if let navigationBarPresentationData = navigationBarPresentationData { diff --git a/submodules/DrawingUI/Sources/ColorPickerScreen.swift b/submodules/DrawingUI/Sources/ColorPickerScreen.swift index 6b79a8f5360..d7eb09b3534 100644 --- a/submodules/DrawingUI/Sources/ColorPickerScreen.swift +++ b/submodules/DrawingUI/Sources/ColorPickerScreen.swift @@ -306,7 +306,7 @@ private class ColorSliderComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { view.updated = self.updated return view.updateLayout(size: availableSize, leftColor: self.leftColor, rightColor: self.rightColor, currentColor: self.currentColor, value: self.value) } @@ -456,7 +456,7 @@ private class ColorFieldComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { view.updated = self.updated return view.updateLayout(size: availableSize, component: self) } @@ -548,7 +548,7 @@ private class ColorPreviewComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.updateLayout(size: availableSize, color: self.color) } } @@ -724,7 +724,7 @@ final class ColorGridComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { view.selected = self.selected return view.updateLayout(size: availableSize, selectedColor: self.color) } @@ -935,7 +935,7 @@ final class ColorSpectrumComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { view.selected = self.selected return view.updateLayout(size: availableSize, selectedColor: self.color) } @@ -1533,7 +1533,7 @@ private class SegmentedControlComponent: Component { preconditionFailure() } - func update(component: SegmentedControlComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: SegmentedControlComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.node.items = component.values.map { SegmentedControlItem(title: $0) } self.node.selectedIndex = component.selectedIndex let selectionChanged = component.selectionChanged @@ -1556,7 +1556,7 @@ private class SegmentedControlComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -1748,7 +1748,7 @@ final class ColorSwatchComponent: Component { self.contentView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) } - func update(component: ColorSwatchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ColorSwatchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let contentSize: CGSize if case .pallete = component.type { @@ -1850,7 +1850,7 @@ final class ColorSwatchComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 876b2c01e98..908fc048835 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -28,6 +28,8 @@ private func makeEntityView(context: AccountContext, entity: DrawingEntity) -> D return DrawingMediaEntityView(context: context, entity: entity) } else if let entity = entity as? DrawingLocationEntity { return DrawingLocationEntityView(context: context, entity: entity) + } else if let entity = entity as? DrawingLinkEntity { + return DrawingLinkEntityView(context: context, entity: entity) } else { return nil } @@ -54,6 +56,9 @@ private func prepareForRendering(entityView: DrawingEntityView) { if let entityView = entityView as? DrawingLocationEntityView { entityView.entity.renderImage = entityView.getRenderImage() } + if let entityView = entityView as? DrawingLinkEntityView { + entityView.entity.renderImage = entityView.getRenderImage() + } } public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { @@ -384,6 +389,14 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { location.width = floor(self.size.width * 0.85) location.scale = zoomScale } + } else if let location = entity as? DrawingLinkEntity { + location.position = center + if setup { + location.rotation = rotation + location.referenceDrawingSize = self.size + location.width = floor(self.size.width * 0.85) + location.scale = zoomScale + } } } @@ -811,7 +824,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { break } - let transition = Transition.easeInOut(duration: 0.2) + let transition = ComponentTransition.easeInOut(duration: 0.2) if isTrappedInBin, let binView = self.bin.view { if !selectedEntityView.isTrappedInBin { let refs = [ @@ -908,7 +921,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { self.bringSubviewToFront(binView) } binView.frame = binFrame - Transition.easeInOut(duration: 0.2).setAlpha(view: binView, alpha: location != nil ? 1.0 : 0.0, delay: location == nil && wasOpened ? 0.4 : 0.0) + ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: binView, alpha: location != nil ? 1.0 : 0.0, delay: location == nil && wasOpened ? 0.4 : 0.0) } return isOpened } @@ -1176,7 +1189,7 @@ private final class EntityBinComponent: Component { } private var wasOpened = false - func update(component: EntityBinComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EntityBinComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -1221,7 +1234,7 @@ private final class EntityBinComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift b/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift new file mode 100644 index 00000000000..22d81cd41d4 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift @@ -0,0 +1,624 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import AccountContext +import TelegramCore +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import StickerResources +import MediaEditor + +private func generateIcon(style: DrawingLinkEntity.Style) -> UIImage? { + guard let image = UIImage(bundleImageName: "Premium/Link") else { + return nil + } + return generateImage(image.size, contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + if let cgImage = image.cgImage { + context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) + } + if [.black, .white].contains(style) { + let green: UIColor + let blue: UIColor + + if case .black = style { + green = UIColor(rgb: 0x64d2ff) + blue = UIColor(rgb: 0x64d2ff) + } else { + green = UIColor(rgb: 0x0a84ff) + blue = UIColor(rgb: 0x0a84ff) + } + + var locations: [CGFloat] = [0.0, 1.0] + let colorsArray = [green.cgColor, blue.cgColor] as NSArray + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions()) + } else { + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + } + }) +} + +public final class DrawingLinkEntityView: DrawingEntityView, UITextViewDelegate { + private var linkEntity: DrawingLinkEntity { + return self.entity as! DrawingLinkEntity + } + + let imageView: UIImageView + + let backgroundView: UIView + let blurredBackgroundView: BlurredBackgroundView + + let textView: DrawingTextView + let iconView: UIImageView + private let imageNode: TransformImageNode + + private let cachedDisposable = MetaDisposable() + + init(context: AccountContext, entity: DrawingLinkEntity) { + self.imageView = UIImageView() + + self.backgroundView = UIView() + self.backgroundView.clipsToBounds = true + + self.blurredBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.25), enableBlur: true) + self.blurredBackgroundView.clipsToBounds = true + + self.textView = DrawingTextView(frame: .zero) + self.textView.clipsToBounds = false + + self.textView.backgroundColor = .clear + self.textView.isEditable = false + self.textView.isSelectable = false + self.textView.contentInset = .zero + self.textView.showsHorizontalScrollIndicator = false + self.textView.showsVerticalScrollIndicator = false + self.textView.scrollsToTop = false + self.textView.isScrollEnabled = false + self.textView.textContainerInset = .zero + self.textView.minimumZoomScale = 1.0 + self.textView.maximumZoomScale = 1.0 + self.textView.keyboardAppearance = .dark + self.textView.autocorrectionType = .default + self.textView.spellCheckingType = .no + self.textView.textContainer.maximumNumberOfLines = 2 + self.textView.textContainer.lineBreakMode = .byTruncatingTail + + self.iconView = UIImageView() + self.imageNode = TransformImageNode() + + super.init(context: context, entity: entity) + + self.textView.delegate = self + self.addSubview(self.imageView) + self.addSubview(self.backgroundView) + self.addSubview(self.blurredBackgroundView) + self.addSubview(self.textView) + self.addSubview(self.iconView) + + self.update(animated: false) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var textSize: CGSize = .zero + public override func sizeThatFits(_ size: CGSize) -> CGSize { + if self.linkEntity.webpage != nil, let image = self.linkEntity.whiteImage { + self.imageView.frame = CGRect(origin: .zero, size: image.size) + return image.size + } else { + var result = self.textView.sizeThatFits(CGSize(width: self.linkEntity.width, height: .greatestFiniteMagnitude)) + self.textSize = result + + let widthExtension = result.height * 0.65 + result.width = floorToScreenPixels(max(104.0, ceil(result.width) + 20.0) + widthExtension) + result.height = ceil(result.height * 1.2); + return result; + } + } + + public override func sizeToFit() { + let center = self.center + let transform = self.transform + self.transform = .identity + super.sizeToFit() + self.center = center + self.transform = transform + } + + public override func layoutSubviews() { + super.layoutSubviews() + + let iconSize: CGFloat + let iconOffset: CGFloat + iconSize = min(76.0, floor(self.bounds.height * 0.6)) + iconOffset = 0.3 + + self.iconView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconSize * iconOffset), y: floorToScreenPixels((self.bounds.height - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize)) + self.imageNode.frame = self.iconView.frame.offsetBy(dx: 0.0, dy: 2.0) + + let imageSize = CGSize(width: iconSize, height: iconSize) + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() + + self.textView.frame = CGRect(origin: CGPoint(x: self.bounds.width - self.textSize.width - 6.0, y: floorToScreenPixels((self.bounds.height - self.textSize.height) / 2.0)), size: self.textSize) + self.backgroundView.frame = self.bounds + self.blurredBackgroundView.frame = self.bounds + self.blurredBackgroundView.update(size: self.bounds.size, transition: .immediate) + } + + override func selectedTapAction() -> Bool { + let values = [self.entity.scale, self.entity.scale * 0.93, self.entity.scale] + let keyTimes = [0.0, 0.33, 1.0] + self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.3, keyPath: "transform.scale") + + let updatedStyle: DrawingLinkEntity.Style + if self.linkEntity.webpage != nil { + switch self.linkEntity.style { + case .white: + updatedStyle = .black + default: + updatedStyle = .white + } + } else { + switch self.linkEntity.style { + case .white: + updatedStyle = .black + case .black: + updatedStyle = .transparent + case .transparent: + if self.linkEntity.hasCustomColor { + updatedStyle = .custom + } else { + updatedStyle = .white + } + case .custom: + updatedStyle = .white + case .blur: + updatedStyle = .white + } + } + self.linkEntity.style = updatedStyle + + self.update() + + return true + } + + private var displayFontSize: CGFloat { + var textFontSize: CGFloat = 0.07 + let textLength = self.linkEntity.url.count + if textLength > 10 { + textFontSize = max(0.01, 0.07 - CGFloat(textLength - 10) / 100.0) + } + + let minFontSize = max(10.0, max(self.linkEntity.referenceDrawingSize.width, self.linkEntity.referenceDrawingSize.height) * 0.025) + let maxFontSize = max(10.0, max(self.linkEntity.referenceDrawingSize.width, self.linkEntity.referenceDrawingSize.height) * 0.25) + let fontSize = minFontSize + (maxFontSize - minFontSize) * textFontSize + return fontSize + } + + private func updateText() { + let string: String + if !self.linkEntity.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + string = self.linkEntity.name.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() + } else { + string = self.linkEntity.url.uppercased() + } + let text = NSMutableAttributedString(string: string) + let range = NSMakeRange(0, text.length) + let fontSize = self.displayFontSize + + self.textView.drawingLayoutManager.textContainers.first?.lineFragmentPadding = floor(fontSize * 0.24) + + let font = Font.with(size: fontSize, design: .camera, weight: .semibold) + text.addAttribute(.font, value: font, range: range) + text.addAttribute(.kern, value: -3.5 as NSNumber, range: range) + self.textView.font = font + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .left + text.addAttribute(.paragraphStyle, value: paragraphStyle, range: range) + + let textColor: UIColor + switch self.linkEntity.style { + case .white: + textColor = UIColor(rgb: 0x0a84ff) + case .black, .blur: + textColor = UIColor(rgb: 0x64d2ff) + case .transparent: + textColor = .white + case .custom: + let color = self.linkEntity.color.toUIColor() + if color.lightness > 0.705 { + textColor = .black + } else { + textColor = .white + } + } + + text.addAttribute(.foregroundColor, value: textColor, range: range) + + self.textView.attributedText = text + self.textView.visualText = text + } + + private var currentStyle: DrawingLinkEntity.Style? + public override func update(animated: Bool = false) { + self.center = self.linkEntity.position + self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.linkEntity.rotation), self.linkEntity.scale, self.linkEntity.scale) + + if self.linkEntity.webpage != nil { + self.textView.isHidden = true + self.backgroundView.isHidden = true + self.blurredBackgroundView.isHidden = true + self.iconView.isHidden = true + + if self.linkEntity.style == .white && self.imageView.image !== self.linkEntity.whiteImage { + self.imageView.image = self.linkEntity.whiteImage + } else if self.linkEntity.style == .black && self.imageView.image !== self.linkEntity.blackImage { + self.imageView.image = self.linkEntity.blackImage + } + } else { + self.textView.isHidden = false + self.textView.frameInsets = UIEdgeInsets(top: 0.15, left: 0.0, bottom: 0.15, right: 0.0) + switch self.linkEntity.style { + case .white: + self.textView.textColor = UIColor(rgb: 0x0a84ff) + self.backgroundView.backgroundColor = .white + self.backgroundView.isHidden = false + self.blurredBackgroundView.isHidden = true + case .black: + self.textView.textColor = UIColor(rgb: 0x64d2ff) + self.backgroundView.backgroundColor = .black + self.backgroundView.isHidden = false + self.blurredBackgroundView.isHidden = true + case .transparent: + self.textView.textColor = .white + self.backgroundView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.2) + self.backgroundView.isHidden = false + self.blurredBackgroundView.isHidden = true + case .custom: + let color = self.linkEntity.color.toUIColor() + let textColor: UIColor + if color.lightness > 0.705 { + textColor = .black + } else { + textColor = .white + } + self.textView.textColor = textColor + self.backgroundView.backgroundColor = color + self.backgroundView.isHidden = false + self.blurredBackgroundView.isHidden = true + case .blur: + self.textView.textColor = .white + self.backgroundView.isHidden = true + self.backgroundView.backgroundColor = UIColor(rgb: 0xffffff) + self.blurredBackgroundView.isHidden = false + } + self.textView.textAlignment = .left + + self.updateText() + + self.iconView.isHidden = false + if self.currentStyle != self.linkEntity.style { + self.currentStyle = self.linkEntity.style + self.iconView.image = generateIcon(style: self.linkEntity.style) + } + + self.backgroundView.layer.cornerRadius = self.textSize.height * 0.2 + self.blurredBackgroundView.layer.cornerRadius = self.backgroundView.layer.cornerRadius + if #available(iOS 13.0, *) { + self.backgroundView.layer.cornerCurve = .continuous + self.blurredBackgroundView.layer.cornerCurve = .continuous + } + } + + self.sizeToFit() + + super.update(animated: animated) + } + + override func updateSelectionView() { + guard let selectionView = self.selectionView as? DrawingLinkEntitySelectionView else { + return + } + self.pushIdentityTransformForMeasurement() + + selectionView.transform = .identity + let bounds = self.selectionBounds + let center = bounds.center + + let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0 + selectionView.center = self.convert(center, to: selectionView.superview) + + selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (bounds.width * self.linkEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.linkEntity.scale) * scale + selectionView.selectionInset * 2.0)) + selectionView.transform = CGAffineTransformMakeRotation(self.linkEntity.rotation) + + self.popIdentityTransformForMeasurement() + } + + override func makeSelectionView() -> DrawingEntitySelectionView? { + if let selectionView = self.selectionView { + return selectionView + } + let selectionView = DrawingLinkEntitySelectionView() + selectionView.entityView = self + return selectionView + } + + func getRenderImage() -> UIImage? { + let rect = self.bounds + UIGraphicsBeginImageContextWithOptions(rect.size, false, 2.0) + self.drawHierarchy(in: rect, afterScreenUpdates: true) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image + } + + func getRenderSubEntities() -> [DrawingEntity] { + return [] + } +} + +final class DrawingLinkEntitySelectionView: DrawingEntitySelectionView { + private let border = SimpleShapeLayer() + private let leftHandle = SimpleShapeLayer() + private let rightHandle = SimpleShapeLayer() + + private var longPressGestureRecognizer: UILongPressGestureRecognizer? + + override init(frame: CGRect) { + let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize) + let handles = [ + self.leftHandle, + self.rightHandle + ] + + super.init(frame: frame) + + self.backgroundColor = .clear + self.isOpaque = false + + self.border.lineCap = .round + self.border.fillColor = UIColor.clear.cgColor + self.border.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.75).cgColor + self.layer.addSublayer(self.border) + + for handle in handles { + handle.bounds = handleBounds + handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor + handle.strokeColor = UIColor(rgb: 0xffffff).cgColor + handle.rasterizationScale = UIScreen.main.scale + handle.shouldRasterize = true + + self.layer.addSublayer(handle) + } + + self.snapTool.onSnapUpdated = { [weak self] type, snapped in + if let self, let entityView = self.entityView { + entityView.onSnapUpdated(type, snapped) + } + } + + let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:))) + self.addGestureRecognizer(longPressGestureRecognizer) + self.longPressGestureRecognizer = longPressGestureRecognizer + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var scale: CGFloat = 1.0 { + didSet { + self.setNeedsLayout() + } + } + + override var selectionInset: CGFloat { + return 15.0 + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + private let snapTool = DrawingEntitySnapTool() + + @objc private func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { + if case .began = gestureRecognizer.state { + self.longPressed() + } + } + + private var currentHandle: CALayer? + override func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingLinkEntity else { + return + } + let location = gestureRecognizer.location(in: self) + switch gestureRecognizer.state { + case .began: + self.tapGestureRecognizer?.isEnabled = false + self.tapGestureRecognizer?.isEnabled = true + + self.longPressGestureRecognizer?.isEnabled = false + self.longPressGestureRecognizer?.isEnabled = true + + self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position) + + if let sublayers = self.layer.sublayers { + for layer in sublayers { + if layer.frame.contains(location) { + self.currentHandle = layer + self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + entityView.onInteractionUpdated(true) + return + } + } + } + self.currentHandle = self.layer + entityView.onInteractionUpdated(true) + case .changed: + if self.currentHandle == nil { + self.currentHandle = self.layer + } + + let delta = gestureRecognizer.translation(in: entityView.superview) + let parentLocation = gestureRecognizer.location(in: self.superview) + let velocity = gestureRecognizer.velocity(in: entityView.superview) + + var updatedScale = entity.scale + var updatedPosition = entity.position + var updatedRotation = entity.rotation + + if self.currentHandle === self.leftHandle || self.currentHandle === self.rightHandle { + if gestureRecognizer.numberOfTouches > 1 { + return + } + var deltaX = gestureRecognizer.translation(in: self).x + if self.currentHandle === self.leftHandle { + deltaX *= -1.0 + } + let scaleDelta = (self.bounds.size.width + deltaX * 2.0) / self.bounds.size.width + updatedScale = max(0.01, updatedScale * scaleDelta) + + let newAngle: CGFloat + if self.currentHandle === self.leftHandle { + newAngle = atan2(self.center.y - parentLocation.y, self.center.x - parentLocation.x) + } else { + newAngle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x) + } + var delta = newAngle - updatedRotation + if delta < -.pi { + delta = 2.0 * .pi + delta + } + let velocityValue = sqrt(velocity.x * velocity.x + velocity.y * velocity.y) / 1000.0 + updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocityValue, delta: delta, updatedRotation: newAngle, skipMultiplier: 1.0) + } else if self.currentHandle === self.layer { + updatedPosition.x += delta.x + updatedPosition.y += delta.y + + updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size) + } + + entity.scale = updatedScale + entity.position = updatedPosition + entity.rotation = updatedRotation + entityView.update() + + gestureRecognizer.setTranslation(.zero, in: entityView) + case .ended, .cancelled: + self.snapTool.reset() + if self.currentHandle != nil { + self.snapTool.rotationReset() + } + entityView.onInteractionUpdated(false) + default: + break + } + + entityView.onPositionUpdated(entity.position) + } + + override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + guard let entityView = self.entityView as? DrawingLinkEntityView, let entity = entityView.entity as? DrawingLinkEntity else { + return + } + + switch gestureRecognizer.state { + case .began, .changed: + if case .began = gestureRecognizer.state { + entityView.onInteractionUpdated(true) + } + let scale = gestureRecognizer.scale + entity.scale = max(0.1, entity.scale * scale) + entityView.update() + + gestureRecognizer.scale = 1.0 + case .ended, .cancelled: + entityView.onInteractionUpdated(false) + default: + break + } + } + + override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { + guard let entityView = self.entityView as? DrawingLinkEntityView, let entity = entityView.entity as? DrawingLinkEntity else { + return + } + + let velocity = gestureRecognizer.velocity + var updatedRotation = entity.rotation + var rotation: CGFloat = 0.0 + + switch gestureRecognizer.state { + case .began: + self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + entityView.onInteractionUpdated(true) + case .changed: + rotation = gestureRecognizer.rotation + updatedRotation += rotation + + updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) + entity.rotation = updatedRotation + entityView.update() + + gestureRecognizer.rotation = 0.0 + case .ended, .cancelled: + self.snapTool.rotationReset() + entityView.onInteractionUpdated(false) + default: + break + } + + entityView.onPositionUpdated(entity.position) + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point) + } + + override func layoutSubviews() { + let inset = self.selectionInset - 10.0 + + let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale)) + let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale) + let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil) + let lineWidth = (1.0 + UIScreenPixel) / self.scale + + let handles = [ + self.leftHandle, + self.rightHandle + ] + + for handle in handles { + handle.path = handlePath + handle.bounds = bounds + handle.lineWidth = lineWidth + } + + self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY) + self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY) + + let width: CGFloat = self.bounds.width - inset * 2.0 + let height: CGFloat = self.bounds.height - inset * 2.0 + let cornerRadius: CGFloat = 12.0 - self.scale + + let perimeter: CGFloat = 2.0 * (width + height - cornerRadius * (4.0 - .pi)) + let count = 12 + let relativeDashLength: CGFloat = 0.25 + let dashLength = perimeter / CGFloat(count) + self.border.lineDashPattern = [dashLength * relativeDashLength, dashLength * relativeDashLength] as [NSNumber] + + self.border.lineWidth = 2.0 / self.scale + self.border.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: width, height: height)), cornerRadius: cornerRadius).cgPath + } +} diff --git a/submodules/DrawingUI/Sources/DrawingLocationEntityView.swift b/submodules/DrawingUI/Sources/DrawingLocationEntityView.swift index 3ed974a97c1..fae19f3b7de 100644 --- a/submodules/DrawingUI/Sources/DrawingLocationEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingLocationEntityView.swift @@ -345,7 +345,7 @@ public final class DrawingLocationEntityView: DrawingEntityView, UITextViewDeleg } override func updateSelectionView() { - guard let selectionView = self.selectionView as? DrawingLocationEntititySelectionView else { + guard let selectionView = self.selectionView as? DrawingLocationEntitySelectionView else { return } self.pushIdentityTransformForMeasurement() @@ -367,7 +367,7 @@ public final class DrawingLocationEntityView: DrawingEntityView, UITextViewDeleg if let selectionView = self.selectionView { return selectionView } - let selectionView = DrawingLocationEntititySelectionView() + let selectionView = DrawingLocationEntitySelectionView() selectionView.entityView = self return selectionView } @@ -386,7 +386,7 @@ public final class DrawingLocationEntityView: DrawingEntityView, UITextViewDeleg } } -final class DrawingLocationEntititySelectionView: DrawingEntitySelectionView { +final class DrawingLocationEntitySelectionView: DrawingEntitySelectionView { private let border = SimpleShapeLayer() private let leftHandle = SimpleShapeLayer() private let rightHandle = SimpleShapeLayer() diff --git a/submodules/DrawingUI/Sources/DrawingReactionView.swift b/submodules/DrawingUI/Sources/DrawingReactionView.swift index 959c3525c74..34148e1fb88 100644 --- a/submodules/DrawingUI/Sources/DrawingReactionView.swift +++ b/submodules/DrawingUI/Sources/DrawingReactionView.swift @@ -127,7 +127,7 @@ public class DrawingReactionEntityView: DrawingStickerEntityView { reactionContextNode.updateLayout(size: availableSize, insets: insets, anchorRect: anchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: transition) } - let reactionContextNodeTransition: Transition = .immediate + let reactionContextNodeTransition: ComponentTransition = .immediate let reactionContextNode: ReactionContextNode reactionContextNode = ReactionContextNode( context: self.context, diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 982235d5996..43c4f710c80 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -416,7 +416,7 @@ private final class BlurredGradientComponent: Component { private var gradientMask = UIImageView() private var gradientForeground = SimpleGradientLayer() - public func update(component: BlurredGradientComponent, availableSize: CGSize, transition: Transition) -> CGSize { + public func update(component: BlurredGradientComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component self.isUserInteractionEnabled = false @@ -452,7 +452,7 @@ private final class BlurredGradientComponent: Component { return View(color: nil, enableBlur: true) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -1303,12 +1303,12 @@ private final class DrawingScreenComponent: CombinedComponent { ) context.add(textSettings .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - textSettings.size.height / 2.0 - 89.0 - additionalBottomInset)) - .appear(Transition.Appear({ _, view, transition in + .appear(ComponentTransition.Appear({ _, view, transition in if let view = view as? TextSettingsComponent.View, !transition.animation.isImmediate { view.animateIn() } })) - .disappear(Transition.Disappear({ view, transition, completion in + .disappear(ComponentTransition.Disappear({ view, transition, completion in if let view = view as? TextSettingsComponent.View, !transition.animation.isImmediate { view.animateOut(completion: completion) } else { @@ -1353,11 +1353,11 @@ private final class DrawingScreenComponent: CombinedComponent { ) context.add(swatch1Button .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch1Button.size.height / 2.0 - 57.0 - additionalBottomInset)) - .appear(Transition.Appear { _, view, transition in + .appear(ComponentTransition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0) transition.animateAlpha(view: view, from: 0.0, to: 1.0) }) - .disappear(Transition.Disappear { view, transition, completion in + .disappear(ComponentTransition.Disappear { view, transition, completion in transition.setScale(view: view, scale: 0.1) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() @@ -1381,11 +1381,11 @@ private final class DrawingScreenComponent: CombinedComponent { ) context.add(swatch2Button .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch2Button.size.height / 2.0 - 57.0 - additionalBottomInset)) - .appear(Transition.Appear { _, view, transition in + .appear(ComponentTransition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.025) transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.025) }) - .disappear(Transition.Disappear { view, transition, completion in + .disappear(ComponentTransition.Disappear { view, transition, completion in transition.setScale(view: view, scale: 0.1) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() @@ -1409,11 +1409,11 @@ private final class DrawingScreenComponent: CombinedComponent { ) context.add(swatch3Button .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch3Button.size.height / 2.0 - 57.0 - additionalBottomInset)) - .appear(Transition.Appear { _, view, transition in + .appear(ComponentTransition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.05) transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.05) }) - .disappear(Transition.Disappear { view, transition, completion in + .disappear(ComponentTransition.Disappear { view, transition, completion in transition.setScale(view: view, scale: 0.1) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() @@ -1437,11 +1437,11 @@ private final class DrawingScreenComponent: CombinedComponent { ) context.add(swatch4Button .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch4Button.size.height / 2.0 - 57.0 - additionalBottomInset)) - .appear(Transition.Appear { _, view, transition in + .appear(ComponentTransition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.075) transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.075) }) - .disappear(Transition.Disappear { view, transition, completion in + .disappear(ComponentTransition.Disappear { view, transition, completion in transition.setScale(view: view, scale: 0.1) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() @@ -1465,11 +1465,11 @@ private final class DrawingScreenComponent: CombinedComponent { ) context.add(swatch5Button .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch5Button.size.height / 2.0 - 57.0 - additionalBottomInset)) - .appear(Transition.Appear { _, view, transition in + .appear(ComponentTransition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.1) transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.1) }) - .disappear(Transition.Disappear { view, transition, completion in + .disappear(ComponentTransition.Disappear { view, transition, completion in transition.setScale(view: view, scale: 0.1) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() @@ -1494,11 +1494,11 @@ private final class DrawingScreenComponent: CombinedComponent { ) context.add(swatch6Button .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch6Button.size.height / 2.0 - 57.0 - additionalBottomInset)) - .appear(Transition.Appear { _, view, transition in + .appear(ComponentTransition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.125) transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.125) }) - .disappear(Transition.Disappear { view, transition, completion in + .disappear(ComponentTransition.Disappear { view, transition, completion in transition.setScale(view: view, scale: 0.1) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() @@ -1522,11 +1522,11 @@ private final class DrawingScreenComponent: CombinedComponent { ) context.add(swatch7Button .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch7Button.size.height / 2.0 - 57.0 - additionalBottomInset)) - .appear(Transition.Appear { _, view, transition in + .appear(ComponentTransition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.15) transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.15) }) - .disappear(Transition.Disappear { view, transition, completion in + .disappear(ComponentTransition.Disappear { view, transition, completion in transition.setScale(view: view, scale: 0.1) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() @@ -1550,11 +1550,11 @@ private final class DrawingScreenComponent: CombinedComponent { ) context.add(swatch8Button .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch7Button.size.height / 2.0 - 57.0 - additionalBottomInset)) - .appear(Transition.Appear { _, view, transition in + .appear(ComponentTransition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.175) transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.175) }) - .disappear(Transition.Disappear { view, transition, completion in + .disappear(ComponentTransition.Disappear { view, transition, completion in transition.setScale(view: view, scale: 0.1) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() @@ -1592,12 +1592,12 @@ private final class DrawingScreenComponent: CombinedComponent { ) context.add(tools .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - tools.size.height / 2.0 - 78.0 - additionalBottomInset)) - .appear(Transition.Appear({ _, view, transition in + .appear(ComponentTransition.Appear({ _, view, transition in if let view = view as? ToolsComponent.View, !transition.animation.isImmediate { view.animateIn(completion: {}) } })) - .disappear(Transition.Disappear({ view, transition, completion in + .disappear(ComponentTransition.Disappear({ view, transition, completion in if let view = view as? ToolsComponent.View, !transition.animation.isImmediate { view.animateOut(completion: completion) } else { @@ -2012,13 +2012,13 @@ private final class DrawingScreenComponent: CombinedComponent { } context.add(doneButton .position(doneButtonPosition) - .appear(Transition.Appear { _, view, transition in + .appear(ComponentTransition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0) transition.animateAlpha(view: view, from: 0.0, to: 1.0) transition.animatePosition(view: view, from: CGPoint(x: 12.0, y: 0.0), to: CGPoint(), additive: true) }) - .disappear(Transition.Disappear { view, transition, completion in + .disappear(ComponentTransition.Disappear { view, transition, completion in transition.setScale(view: view, scale: 0.1) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() @@ -2614,13 +2614,13 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U return result } - func requestUpdate(transition: Transition = .immediate) { + func requestUpdate(transition: ComponentTransition = .immediate) { if let (layout, orientation) = self.validLayout { self.containerLayoutUpdated(layout: layout, orientation: orientation, transition: transition) } } - func containerLayoutUpdated(layout: ContainerViewLayout, orientation: UIInterfaceOrientation?, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { + func containerLayoutUpdated(layout: ContainerViewLayout, orientation: UIInterfaceOrientation?, forceUpdate: Bool = false, animateOut: Bool = false, transition: ComponentTransition) { guard let controller = self.controller else { return } @@ -2922,7 +2922,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - (self.displayNode as! Node).containerLayoutUpdated(layout: layout, orientation: self.orientation, transition: Transition(transition)) + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, orientation: self.orientation, transition: ComponentTransition(transition)) } public func adapterContainerLayoutUpdatedSize(_ size: CGSize, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat, inputHeight: CGFloat, orientation: UIInterfaceOrientation, isRegular: Bool, animated: Bool) { @@ -3088,6 +3088,7 @@ public final class DrawingToolsInteraction { var isVideo = false var isAdditional = false var isMessage = false + var isLink = false if let entity = entityView.entity as? DrawingStickerEntity { if case let .dualVideoReference(isAdditionalValue) = entity.content { isVideo = true @@ -3095,6 +3096,8 @@ public final class DrawingToolsInteraction { } else if case .message = entity.content { isMessage = true } + } else if entityView.entity is DrawingLinkEntity { + isLink = true } guard (!isVideo || isAdditional) && (!isMessage || !isTopmost) else { @@ -3112,7 +3115,7 @@ public final class DrawingToolsInteraction { } })) } - if let entityView = entityView as? DrawingLocationEntityView { + if entityView is DrawingLocationEntityView || entityView is DrawingLinkEntityView { actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Edit, accessibilityLabel: presentationData.strings.Paint_Edit), action: { [weak self, weak entityView] in if let self, let entityView { self.editEntity(entityView.entity) @@ -3140,7 +3143,7 @@ public final class DrawingToolsInteraction { } })) } - if !isVideo && !isMessage { + if !isVideo && !isMessage && !isLink { if let stickerEntity = entityView.entity as? DrawingStickerEntity, case let .file(_, type) = stickerEntity.content, case .reaction = type { } else { @@ -3531,7 +3534,7 @@ public final class DrawingToolsInteraction { } } - public func containerLayoutUpdated(layout: ContainerViewLayout, transition: Transition) { + public func containerLayoutUpdated(layout: ContainerViewLayout, transition: ComponentTransition) { self.validLayout = layout guard self.isActive else { diff --git a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntityView.swift b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntityView.swift index b2ce0322e90..09d2c47c8cf 100644 --- a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntityView.swift @@ -103,7 +103,7 @@ final class DrawingSimpleShapeEntityView: DrawingEntityView { override func updateSelectionView() { super.updateSelectionView() - guard let selectionView = self.selectionView as? DrawingSimpleShapeEntititySelectionView else { + guard let selectionView = self.selectionView as? DrawingSimpleShapeEntitySelectionView else { return } @@ -117,7 +117,7 @@ final class DrawingSimpleShapeEntityView: DrawingEntityView { if let selectionView = self.selectionView { return selectionView } - let selectionView = DrawingSimpleShapeEntititySelectionView() + let selectionView = DrawingSimpleShapeEntitySelectionView() selectionView.entityView = self return selectionView } @@ -136,7 +136,7 @@ final class DrawingSimpleShapeEntityView: DrawingEntityView { } } -final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView { +final class DrawingSimpleShapeEntitySelectionView: DrawingEntitySelectionView { private let leftHandle = SimpleShapeLayer() private let topLeftHandle = SimpleShapeLayer() private let topHandle = SimpleShapeLayer() diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift index 0299a883cf6..ae5a1f2e6e5 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift @@ -681,7 +681,7 @@ public class DrawingStickerEntityView: DrawingEntityView { func onDeselection() { } - + func innerLayoutSubview(boundingSize: CGSize) -> CGSize { return boundingSize } @@ -753,7 +753,7 @@ public class DrawingStickerEntityView: DrawingEntityView { } override func updateSelectionView() { - guard let selectionView = self.selectionView as? DrawingStickerEntititySelectionView else { + guard let selectionView = self.selectionView as? DrawingStickerEntitySelectionView else { return } self.pushIdentityTransformForMeasurement() @@ -776,7 +776,7 @@ public class DrawingStickerEntityView: DrawingEntityView { if let selectionView = self.selectionView { return selectionView } - let selectionView = DrawingStickerEntititySelectionView() + let selectionView = DrawingStickerEntitySelectionView() selectionView.entityView = self return selectionView } @@ -822,7 +822,7 @@ public class DrawingStickerEntityView: DrawingEntityView { } } -final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView { +final class DrawingStickerEntitySelectionView: DrawingEntitySelectionView { private let border = SimpleShapeLayer() private let leftHandle = SimpleShapeLayer() private let rightHandle = SimpleShapeLayer() @@ -1085,7 +1085,7 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView { let aspectRatio = entity.baseSize.width / entity.baseSize.height let width: CGFloat - let height: CGFloat + var height: CGFloat if entity.baseSize.width > entity.baseSize.height { width = self.bounds.width - inset * 2.0 diff --git a/submodules/DrawingUI/Sources/DrawingTextEntityView.swift b/submodules/DrawingUI/Sources/DrawingTextEntityView.swift index 1b89f1a6bcd..4e9b833497f 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntityView.swift @@ -257,7 +257,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate self.updateEditingPosition(animated: true) - if let selectionView = self.selectionView as? DrawingTextEntititySelectionView { + if let selectionView = self.selectionView as? DrawingTextEntitySelectionView { selectionView.alpha = 0.0 if !self.textEntity.text.string.isEmpty { selectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) @@ -353,7 +353,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate } self.update(animated: false) - if let selectionView = self.selectionView as? DrawingTextEntititySelectionView { + if let selectionView = self.selectionView as? DrawingTextEntitySelectionView { selectionView.alpha = 1.0 selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } @@ -587,7 +587,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate } override func updateSelectionView() { - guard let selectionView = self.selectionView as? DrawingTextEntititySelectionView else { + guard let selectionView = self.selectionView as? DrawingTextEntitySelectionView else { return } self.pushIdentityTransformForMeasurement() @@ -609,7 +609,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate if let selectionView = self.selectionView { return selectionView } - let selectionView = DrawingTextEntititySelectionView() + let selectionView = DrawingTextEntitySelectionView() selectionView.entityView = self return selectionView } @@ -655,7 +655,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate } } -final class DrawingTextEntititySelectionView: DrawingEntitySelectionView { +final class DrawingTextEntitySelectionView: DrawingEntitySelectionView { private let border = SimpleShapeLayer() private let leftHandle = SimpleShapeLayer() private let rightHandle = SimpleShapeLayer() diff --git a/submodules/DrawingUI/Sources/DrawingVectorEntityView.swift b/submodules/DrawingUI/Sources/DrawingVectorEntityView.swift index bc5415c2b3e..2f5b3ea4908 100644 --- a/submodules/DrawingUI/Sources/DrawingVectorEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingVectorEntityView.swift @@ -59,7 +59,7 @@ final class DrawingVectorEntityView: DrawingEntityView { } override func updateSelectionView() { - guard let selectionView = self.selectionView as? DrawingVectorEntititySelectionView else { + guard let selectionView = self.selectionView as? DrawingVectorEntitySelectionView else { return } @@ -96,7 +96,7 @@ final class DrawingVectorEntityView: DrawingEntityView { if let selectionView = self.selectionView { return selectionView } - let selectionView = DrawingVectorEntititySelectionView() + let selectionView = DrawingVectorEntitySelectionView() selectionView.entityView = self return selectionView } @@ -131,7 +131,7 @@ private func midPointPositionFor(start: CGPoint, end: CGPoint, length: CGFloat, return p2 } -final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView { +final class DrawingVectorEntitySelectionView: DrawingEntitySelectionView { private let startHandle = SimpleShapeLayer() private let midHandle = SimpleShapeLayer() private let endHandle = SimpleShapeLayer() diff --git a/submodules/DrawingUI/Sources/DrawingView.swift b/submodules/DrawingUI/Sources/DrawingView.swift index aed105e3c90..e18098d9e75 100644 --- a/submodules/DrawingUI/Sources/DrawingView.swift +++ b/submodules/DrawingUI/Sources/DrawingView.swift @@ -1045,7 +1045,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt } func setBrushSizePreview(_ size: CGFloat?) { - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) if let size = size { let minLineWidth = max(1.0, max(self.frame.width, self.frame.height) * 0.002) let maxLineWidth = max(10.0, max(self.frame.width, self.frame.height) * 0.07) diff --git a/submodules/DrawingUI/Sources/ModeAndSizeComponent.swift b/submodules/DrawingUI/Sources/ModeAndSizeComponent.swift index 15a0e4025df..a8822e5230a 100644 --- a/submodules/DrawingUI/Sources/ModeAndSizeComponent.swift +++ b/submodules/DrawingUI/Sources/ModeAndSizeComponent.swift @@ -181,7 +181,7 @@ final class ModeAndSizeComponent: Component { self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) } - func update(component: ModeAndSizeComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: ModeAndSizeComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component self.updated = component.sizeUpdated @@ -259,7 +259,7 @@ final class ModeAndSizeComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/DrawingUI/Sources/TextSettingsComponent.swift b/submodules/DrawingUI/Sources/TextSettingsComponent.swift index 099be3521ee..801ab0a7507 100644 --- a/submodules/DrawingUI/Sources/TextSettingsComponent.swift +++ b/submodules/DrawingUI/Sources/TextSettingsComponent.swift @@ -141,7 +141,7 @@ final class TextAlignmentComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: TextAlignmentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: TextAlignmentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let height = 2.0 - UIScreenPixel let spacing: CGFloat = 3.0 + UIScreenPixel let long = 21.0 @@ -175,7 +175,7 @@ final class TextAlignmentComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -228,7 +228,7 @@ final class TextFontComponent: Component { } } - func update(component: TextFontComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: TextFontComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component if self.icon.contents == nil { @@ -281,7 +281,7 @@ final class TextFontComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -530,7 +530,7 @@ final class TextSettingsComponent: CombinedComponent { ) context.add(styleButton .position(CGPoint(x: offset + styleButton.size.width / 2.0, y: context.availableSize.height / 2.0)) - .update(Transition.Update { _, view, transition in + .update(ComponentTransition.Update { _, view, transition in if let snapshot = view.snapshotView(afterScreenUpdates: false) { transition.setAlpha(view: snapshot, alpha: 0.0, completion: { [weak snapshot] _ in snapshot?.removeFromSuperview() @@ -749,7 +749,7 @@ public final class TextSizeSliderComponent: Component { return true } - func updateLayout(size: CGSize, component: TextSizeSliderComponent, transition: Transition) -> CGSize { + func updateLayout(size: CGSize, component: TextSizeSliderComponent, transition: ComponentTransition) -> CGSize { self.component = component let previousSize = self.validSize @@ -797,7 +797,7 @@ public final class TextSizeSliderComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { view.updated = self.updated view.released = self.released return view.updateLayout(size: availableSize, component: self, transition: transition) diff --git a/submodules/DrawingUI/Sources/ToolsComponent.swift b/submodules/DrawingUI/Sources/ToolsComponent.swift index b3f88ca268f..47ffcd0fe04 100644 --- a/submodules/DrawingUI/Sources/ToolsComponent.swift +++ b/submodules/DrawingUI/Sources/ToolsComponent.swift @@ -295,7 +295,7 @@ final class ToolsComponent: Component { } func animateOut(completion: @escaping () -> Void) { - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) var delay = 0.0 for i in 0 ..< self.toolViews.count { let view = self.toolViews[i] @@ -306,7 +306,7 @@ final class ToolsComponent: Component { } } - func update(component: ToolsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ToolsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component if self.toolViews.isEmpty { @@ -467,7 +467,7 @@ final class ToolsComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/FileMediaResourceStatus/Sources/FileMediaResourceStatus.swift b/submodules/FileMediaResourceStatus/Sources/FileMediaResourceStatus.swift index a8428853f7a..5c7fb53fd93 100644 --- a/submodules/FileMediaResourceStatus/Sources/FileMediaResourceStatus.swift +++ b/submodules/FileMediaResourceStatus/Sources/FileMediaResourceStatus.swift @@ -67,7 +67,7 @@ public func messageFileMediaResourceStatus(context: AccountContext, file: Telegr } } } else if let pendingStatus = pendingStatus { - mediaStatus = .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: pendingStatus.progress)) + mediaStatus = .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: pendingStatus.progress.progress)) } else { mediaStatus = .fetchStatus(EngineMediaResource.FetchStatus(resourceStatus)) } @@ -104,7 +104,7 @@ public func messageImageMediaResourceStatus(context: AccountContext, image: Tele |> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in let mediaStatus: FileMediaResourceMediaStatus if let pendingStatus = pendingStatus { - mediaStatus = .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: pendingStatus.progress)) + mediaStatus = .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: pendingStatus.progress.progress)) } else { mediaStatus = .fetchStatus(EngineMediaResource.FetchStatus(resourceStatus)) } diff --git a/submodules/GalleryData/Sources/GalleryData.swift b/submodules/GalleryData/Sources/GalleryData.swift index 26087254b04..5e9bfedeccf 100644 --- a/submodules/GalleryData/Sources/GalleryData.swift +++ b/submodules/GalleryData/Sources/GalleryData.swift @@ -91,7 +91,7 @@ public func instantPageGalleryMedia(webpageId: MediaId, page: InstantPage, galle return result } -public func chatMessageGalleryControllerData(context: AccountContext, chatLocation: ChatLocation?, chatFilterTag: MemoryBuffer?, chatLocationContextHolder: Atomic?, message: Message, navigationController: NavigationController?, standalone: Bool, reverseMessageGalleryOrder: Bool, mode: ChatControllerInteractionOpenMessageMode, source: GalleryControllerItemSource?, synchronousLoad: Bool, actionInteraction: GalleryControllerActionInteraction?) -> ChatMessageGalleryControllerData? { +public func chatMessageGalleryControllerData(context: AccountContext, chatLocation: ChatLocation?, chatFilterTag: MemoryBuffer?, chatLocationContextHolder: Atomic?, message: Message, mediaIndex: Int? = nil, navigationController: NavigationController?, standalone: Bool, reverseMessageGalleryOrder: Bool, mode: ChatControllerInteractionOpenMessageMode, source: GalleryControllerItemSource?, synchronousLoad: Bool, actionInteraction: GalleryControllerActionInteraction?) -> ChatMessageGalleryControllerData? { var standalone = standalone if message.id.peerId.namespace == Namespaces.Peer.CloudUser && message.id.namespace != Namespaces.Message.Cloud { standalone = true @@ -111,7 +111,10 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati } } for media in message.media { - if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { + if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first, case .full = extendedMedia { + standalone = true + galleryMedia = paidContent + } else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { standalone = true galleryMedia = fullMedia } else if let action = media as? TelegramMediaAction { @@ -238,7 +241,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati var source = source if standalone { - source = .standaloneMessage(message) + source = .standaloneMessage(message, nil) } if internalDocumentItemSupportsMimeType(file.mimeType, fileName: file.fileName ?? "file") { @@ -277,7 +280,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati return .gallery(startState |> deliverOnMainQueue |> map { startState in - let gallery = GalleryController(context: context, source: source ?? (standalone ? .standaloneMessage(message) : .peerMessagesAtId(messageId: message.id, chatLocation: openChatLocation, customTag: chatFilterTag, chatLocationContextHolder: openChatLocationContextHolder)), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: startState.timecode, playbackRate: startState.rate, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in + let gallery = GalleryController(context: context, source: source ?? (standalone ? .standaloneMessage(message, mediaIndex) : .peerMessagesAtId(messageId: message.id, chatLocation: openChatLocation, customTag: chatFilterTag, chatLocationContextHolder: openChatLocationContextHolder)), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: startState.timecode, playbackRate: startState.rate, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in navigationController?.replaceTopController(controller, animated: false, ready: ready) }, baseNavigationController: navigationController, actionInteraction: actionInteraction) gallery.temporaryDoNotWaitForReady = autoplayingVideo diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index adcdc90599e..65273c497c2 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -820,7 +820,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.currentMessage = message var displayInfo = displayInfo - if Namespaces.Message.allNonRegular.contains(message.id.namespace) { + if Namespaces.Message.allNonRegular.contains(message.id.namespace) || message.timestamp == 0 { displayInfo = false } @@ -906,7 +906,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll canEdit = false } - if message.isCopyProtected() || peerIsCopyProtected { + if message.isCopyProtected() || peerIsCopyProtected || message.paidContent != nil { canShare = false canEdit = false } @@ -938,7 +938,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll var messageText = NSAttributedString(string: "") var hasCaption = false for media in message.media { - if media is TelegramMediaImage { + if media is TelegramMediaPaidContent { + hasCaption = true + } else if media is TelegramMediaImage { hasCaption = true } else if let file = media as? TelegramMediaFile { hasCaption = file.mimeType.hasPrefix("image/") @@ -1636,7 +1638,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll var hasExternalShare = true for media in currentMessage.media { - if let invoice = media as? TelegramMediaInvoice, let _ = invoice.extendedMedia { + if let _ = media as? TelegramMediaPaidContent { + hasExternalShare = false + break + } else if let invoice = media as? TelegramMediaInvoice, let _ = invoice.extendedMedia { hasExternalShare = false break } diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 2ddb27fafed..ff88e9092ec 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -63,22 +63,30 @@ private func galleryMediaForMedia(media: Media) -> Media? { return nil } -private func mediaForMessage(message: Message) -> (Media, TelegramMediaImage?)? { +private func mediaForMessage(message: Message) -> [(Media, TelegramMediaImage?)] { for media in message.media { if let result = galleryMediaForMedia(media: media) { - return (result, nil) + return [(result, nil)] + } else if let paidContent = media as? TelegramMediaPaidContent { + var results: [(Media, TelegramMediaImage?)] = [] + for case let .full(fullMedia) in paidContent.extendedMedia { + if let result = galleryMediaForMedia(media: fullMedia) { + results.append((result, nil)) + } + } + return results } else if let webpage = media as? TelegramMediaWebpage { switch webpage.content { case let .Loaded(content): if let embedUrl = content.embedUrl, !embedUrl.isEmpty { - return (webpage, nil) + return [(webpage, nil)] } else if let file = content.file { if let result = galleryMediaForMedia(media: file) { - return (result, content.image) + return [(result, content.image)] } } else if let image = content.image { if let result = galleryMediaForMedia(media: image) { - return (result, nil) + return [(result, nil)] } } case .Pending: @@ -86,7 +94,7 @@ private func mediaForMessage(message: Message) -> (Media, TelegramMediaImage?)? } } } - return nil + return [] } private let internalExtensions = Set([ @@ -177,7 +185,7 @@ private func galleryMessageCaptionText(_ message: Message) -> String { public func galleryItemForEntry( context: AccountContext, presentationData: PresentationData, - entry: MessageHistoryEntry, + entry: GalleryEntry, isCentral: Bool = false, streamVideos: Bool, loopVideos: Bool = false, @@ -199,195 +207,209 @@ public func galleryItemForEntry( generateStoreAfterDownload: ((Message, TelegramMediaFile) -> (() -> Void)?)? = nil, present: @escaping (ViewController, Any?) -> Void) -> GalleryItem? { - let message = entry.message - let location = entry.location - if let (media, mediaImage) = mediaForMessage(message: message) { - if let _ = media as? TelegramMediaImage { - return ChatImageGalleryItem( - context: context, - presentationData: presentationData, - message: message, - location: location, - translateToLanguage: translateToLanguage, - peerIsCopyProtected: peerIsCopyProtected, - isSecret: isSecret, - displayInfoOnTop: displayInfoOnTop, - performAction: performAction, - openActionOptions: openActionOptions, - present: present - ) - } else if let file = media as? TelegramMediaFile { - if file.isVideo { - let content: UniversalVideoContent - let captureProtected = message.isCopyProtected() || message.containsSecretMedia || message.minAutoremoveOrClearTimeout == viewOnceTimeout - if file.isAnimated { - content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file)) + let message = entry.entry.message + let location = entry.location ?? entry.entry.location + let messageMedia = mediaForMessage(message: message) + + let mediaAndMediaImage: (Media, TelegramMediaImage?)? + if let mediaIndex = entry.mediaIndex { + if mediaIndex < messageMedia.count { + mediaAndMediaImage = messageMedia[Int(mediaIndex)] + } else { + mediaAndMediaImage = nil + } + } else { + mediaAndMediaImage = messageMedia.first + } + guard let (media, mediaImage) = mediaAndMediaImage else { + return nil + } + + if let _ = media as? TelegramMediaImage { + return ChatImageGalleryItem( + context: context, + presentationData: presentationData, + message: message, + mediaIndex: entry.mediaIndex, + location: location, + translateToLanguage: translateToLanguage, + peerIsCopyProtected: peerIsCopyProtected, + isSecret: isSecret, + displayInfoOnTop: displayInfoOnTop, + performAction: performAction, + openActionOptions: openActionOptions, + present: present + ) + } else if let file = media as? TelegramMediaFile { + if file.isVideo { + let content: UniversalVideoContent + let captureProtected = message.isCopyProtected() || message.containsSecretMedia || message.minAutoremoveOrClearTimeout == viewOnceTimeout || message.paidContent != nil + if file.isAnimated { + content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file)) + } else { + if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") { + content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file)) } else { - if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") { - content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file)) - } else { - content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), content: .file(.message(message: MessageReference(message), media: file)), streamVideo: streamVideos, loopVideo: loopVideos) - } + content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), content: .file(.message(message: MessageReference(message), media: file)), streamVideo: streamVideos, loopVideo: loopVideos) } - - var entities: [MessageTextEntity] = [] + } + + var entities: [MessageTextEntity] = [] + for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + entities = attribute.entities + break + } + } + var text = galleryMessageCaptionText(message) + if let translateToLanguage, !text.isEmpty { for attribute in message.attributes { - if let attribute = attribute as? TextEntitiesMessageAttribute { + if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage { + text = attribute.text entities = attribute.entities break } } - var text = galleryMessageCaptionText(message) - if let translateToLanguage, !text.isEmpty { - for attribute in message.attributes { - if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage { - text = attribute.text - entities = attribute.entities - break - } - } - } - - if let result = addLocallyGeneratedEntities(text, enabledTypes: [.timecode], entities: entities, mediaDuration: file.duration.flatMap(Double.init)) { - entities = result - } - - var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp) - if Namespaces.Message.allNonRegular.contains(message.id.namespace) { - originData = GalleryItemOriginData(title: nil, timestamp: nil) - } - - let caption = galleryCaptionStringWithAppliedEntities(context: context, text: text, entities: entities, message: message) - return UniversalVideoGalleryItem( - context: context, - presentationData: presentationData, - content: content, - originData: originData, - indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, - contentInfo: .message(message), - caption: caption, - displayInfoOnTop: displayInfoOnTop, - hideControls: hideControls, - fromPlayingVideo: fromPlayingVideo, - isSecret: isSecret, - landscape: landscape, - timecode: timecode, - peerIsCopyProtected: peerIsCopyProtected, - playbackRate: playbackRate, - configuration: configuration, - playbackCompleted: playbackCompleted, - performAction: performAction, - openActionOptions: openActionOptions, - storeMediaPlaybackState: storeMediaPlaybackState, - present: present - ) - } else { - if let fileName = file.fileName, (fileName as NSString).pathExtension.lowercased() == "json" { - return ChatAnimationGalleryItem(context: context, presentationData: presentationData, message: message, location: location) + } + + if let result = addLocallyGeneratedEntities(text, enabledTypes: [.timecode], entities: entities, mediaDuration: file.duration.flatMap(Double.init)) { + entities = result + } + + var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp) + if Namespaces.Message.allNonRegular.contains(message.id.namespace) { + originData = GalleryItemOriginData(title: nil, timestamp: nil) + } + + let caption = galleryCaptionStringWithAppliedEntities(context: context, text: text, entities: entities, message: message) + return UniversalVideoGalleryItem( + context: context, + presentationData: presentationData, + content: content, + originData: originData, + indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, + contentInfo: .message(message, entry.mediaIndex), + caption: caption, + displayInfoOnTop: displayInfoOnTop, + hideControls: hideControls, + fromPlayingVideo: fromPlayingVideo, + isSecret: isSecret, + landscape: landscape, + timecode: timecode, + peerIsCopyProtected: peerIsCopyProtected, + playbackRate: playbackRate, + configuration: configuration, + playbackCompleted: playbackCompleted, + performAction: performAction, + openActionOptions: openActionOptions, + storeMediaPlaybackState: storeMediaPlaybackState, + present: present + ) + } else { + if let fileName = file.fileName, (fileName as NSString).pathExtension.lowercased() == "json" { + return ChatAnimationGalleryItem(context: context, presentationData: presentationData, message: message, location: location) + } + else if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" { + var pixelsCount: Int = 0 + if let dimensions = file.dimensions { + pixelsCount = Int(dimensions.width) * Int(dimensions.height) } - else if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" { - var pixelsCount: Int = 0 - if let dimensions = file.dimensions { - pixelsCount = Int(dimensions.width) * Int(dimensions.height) - } - if pixelsCount < 10000 * 10000 { - return ChatImageGalleryItem( - context: context, - presentationData: presentationData, - message: message, - location: location, - translateToLanguage: translateToLanguage, - peerIsCopyProtected: peerIsCopyProtected, - isSecret: isSecret, - displayInfoOnTop: displayInfoOnTop, - performAction: performAction, - openActionOptions: openActionOptions, - present: present - ) - } else { - return ChatDocumentGalleryItem( - context: context, - presentationData: presentationData, - message: message, - location: location - ) - } - } else if internalDocumentItemSupportsMimeType(file.mimeType, fileName: file.fileName) { - return ChatDocumentGalleryItem( + if pixelsCount < 10000 * 10000 { + return ChatImageGalleryItem( context: context, presentationData: presentationData, message: message, - location: location + location: location, + translateToLanguage: translateToLanguage, + peerIsCopyProtected: peerIsCopyProtected, + isSecret: isSecret, + displayInfoOnTop: displayInfoOnTop, + performAction: performAction, + openActionOptions: openActionOptions, + present: present ) } else { - return ChatExternalFileGalleryItem( + return ChatDocumentGalleryItem( context: context, presentationData: presentationData, message: message, location: location ) } - } - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = webpage.content { - var content: UniversalVideoContent? - switch websiteType(of: webpageContent.websiteName) { - case .instagram where webpageContent.file != nil && webpageContent.image != nil && webpageContent.file!.isVideo: - content = NativeVideoContent(id: .message(message.stableId, webpageContent.file?.id ?? webpage.webpageId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: webpageContent.file!), imageReference: webpageContent.image.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, enableSound: true, captureProtected: message.isCopyProtected() || message.containsSecretMedia, storeAfterDownload: nil) - default: - if let embedUrl = webpageContent.embedUrl, let image = webpageContent.image { - if let file = webpageContent.file, file.isVideo { - content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: message.isCopyProtected() || message.containsSecretMedia, storeAfterDownload: generateStoreAfterDownload?(message, file)) - } else if URL(string: embedUrl)?.pathExtension == "mp4" { - content = SystemVideoContent(userLocation: .peer(message.id.peerId), url: embedUrl, imageReference: .webPage(webPage: WebpageReference(webpage), media: image), dimensions: webpageContent.embedSize?.cgSize ?? CGSize(width: 640.0, height: 640.0), duration: webpageContent.duration.flatMap(Double.init) ?? 0.0) - } - } - if content == nil, let webEmbedContent = WebEmbedVideoContent(userLocation: .peer(message.id.peerId), webPage: webpage, webpageContent: webpageContent, forcedTimestamp: timecode.flatMap(Int.init), openUrl: { url in - performAction(.url(url: url.absoluteString, concealed: false)) - }) { - content = webEmbedContent - } - } - if let content = content { - var description: NSAttributedString? - if let descriptionText = webpageContent.text { - var entities: [MessageTextEntity] = [] - if let result = addLocallyGeneratedEntities(descriptionText, enabledTypes: [.timecode], entities: entities, mediaDuration: 86400) { - entities = result - } - description = galleryCaptionStringWithAppliedEntities(context: context, text: descriptionText, entities: entities, message: message) - } - - var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp) - if Namespaces.Message.allNonRegular.contains(message.id.namespace) { - originData = GalleryItemOriginData(title: nil, timestamp: nil) - } - - return UniversalVideoGalleryItem( + } else if internalDocumentItemSupportsMimeType(file.mimeType, fileName: file.fileName) { + return ChatDocumentGalleryItem( context: context, presentationData: presentationData, - content: content, - originData: originData, - indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, - contentInfo: .message(message), - caption: NSAttributedString(string: ""), - description: description, - displayInfoOnTop: displayInfoOnTop, - fromPlayingVideo: fromPlayingVideo, - isSecret: isSecret, - landscape: landscape, - timecode: timecode, - playbackRate: playbackRate, - configuration: configuration, - performAction: performAction, - openActionOptions: openActionOptions, - storeMediaPlaybackState: storeMediaPlaybackState, - present: present + message: message, + location: location ) } else { - return nil + return ChatExternalFileGalleryItem( + context: context, + presentationData: presentationData, + message: message, + location: location + ) } } + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = webpage.content { + var content: UniversalVideoContent? + switch websiteType(of: webpageContent.websiteName) { + case .instagram where webpageContent.file != nil && webpageContent.image != nil && webpageContent.file!.isVideo: + content = NativeVideoContent(id: .message(message.stableId, webpageContent.file?.id ?? webpage.webpageId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: webpageContent.file!), imageReference: webpageContent.image.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, enableSound: true, captureProtected: message.isCopyProtected() || message.containsSecretMedia, storeAfterDownload: nil) + default: + if let embedUrl = webpageContent.embedUrl, let image = webpageContent.image { + if let file = webpageContent.file, file.isVideo { + content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: message.isCopyProtected() || message.containsSecretMedia, storeAfterDownload: generateStoreAfterDownload?(message, file)) + } else if URL(string: embedUrl)?.pathExtension == "mp4" { + content = SystemVideoContent(userLocation: .peer(message.id.peerId), url: embedUrl, imageReference: .webPage(webPage: WebpageReference(webpage), media: image), dimensions: webpageContent.embedSize?.cgSize ?? CGSize(width: 640.0, height: 640.0), duration: webpageContent.duration.flatMap(Double.init) ?? 0.0) + } + } + if content == nil, let webEmbedContent = WebEmbedVideoContent(userLocation: .peer(message.id.peerId), webPage: webpage, webpageContent: webpageContent, forcedTimestamp: timecode.flatMap(Int.init), openUrl: { url in + performAction(.url(url: url.absoluteString, concealed: false)) + }) { + content = webEmbedContent + } + } + if let content = content { + var description: NSAttributedString? + if let descriptionText = webpageContent.text { + var entities: [MessageTextEntity] = [] + if let result = addLocallyGeneratedEntities(descriptionText, enabledTypes: [.timecode], entities: entities, mediaDuration: 86400) { + entities = result + } + description = galleryCaptionStringWithAppliedEntities(context: context, text: descriptionText, entities: entities, message: message) + } + + var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp) + if Namespaces.Message.allNonRegular.contains(message.id.namespace) { + originData = GalleryItemOriginData(title: nil, timestamp: nil) + } + + return UniversalVideoGalleryItem( + context: context, + presentationData: presentationData, + content: content, + originData: originData, + indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, + contentInfo: .message(message, entry.mediaIndex), + caption: NSAttributedString(string: ""), + description: description, + displayInfoOnTop: displayInfoOnTop, + fromPlayingVideo: fromPlayingVideo, + isSecret: isSecret, + landscape: landscape, + timecode: timecode, + playbackRate: playbackRate, + configuration: configuration, + performAction: performAction, + openActionOptions: openActionOptions, + storeMediaPlaybackState: storeMediaPlaybackState, + present: present + ) + } } + return nil } @@ -497,6 +519,38 @@ public struct GalleryConfiguration { } } +public struct GalleryEntryStableId: Hashable { + public var stableId: UInt32 + public var mediaIndex: Int? +} + +public struct GalleryEntry { + public var entry: MessageHistoryEntry + public var mediaIndex: Int? + public var location: MessageHistoryEntryLocation? + + public var stableId: GalleryEntryStableId { + return GalleryEntryStableId(stableId: self.entry.message.stableId, mediaIndex: self.mediaIndex) + } +} + +private func galleryEntriesForMessageHistoryEntries(_ entries: [MessageHistoryEntry]) -> [GalleryEntry] { + var results: [GalleryEntry] = [] + for entry in entries { + let mediaCount = mediaForMessage(message: entry.message).count + if mediaCount > 0 { + if mediaCount > 1 { + for i in 0 ..< mediaCount { + results.append(GalleryEntry(entry: entry, mediaIndex: i, location: MessageHistoryEntryLocation(index: i, count: mediaCount))) + } + } else { + results.append(GalleryEntry(entry: entry)) + } + } + } + return results +} + public class GalleryController: ViewController, StandalonePresentableController, KeyShortcutResponder { public static let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), enableBackgroundBlur: false, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) public static let lightNavigationTheme = NavigationBarTheme(buttonColor: UIColor(rgb: 0x007aff), disabledButtonColor: UIColor(rgb: 0xd0d0d0), primaryTextColor: .black, backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), enableBackgroundBlur: false, separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) @@ -530,12 +584,12 @@ public class GalleryController: ViewController, StandalonePresentableController, private let disposable = MetaDisposable() private var peerIsCopyProtected = false - private var entries: [MessageHistoryEntry] = [] + private var entries: [GalleryEntry] = [] private var hasLeftEntries: Bool = false private var hasRightEntries: Bool = false private var loadingMore: Bool = false private var tag: HistoryViewInputTag? - private var centralEntryStableId: UInt32? + private var centralEntryStableId: GalleryEntryStableId? private var configuration: GalleryConfiguration? private let centralItemTitle = Promise() @@ -654,7 +708,7 @@ public class GalleryController: ViewController, StandalonePresentableController, return nil } } - case let .standaloneMessage(m): + case let .standaloneMessage(m, _): message = .single((m, m.isCopyProtected())) case let .custom(messages, messageId, _): message = messages @@ -684,7 +738,7 @@ public class GalleryController: ViewController, StandalonePresentableController, } else { inputTag = .tag(tags) } - return context.account.postbox.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), anchor: .index(message.index), ignoreMessagesInTimestampRange: nil, count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: inputTag, appendMessagesFromTheSameGroup: false, namespaces: namespaces, orderStatistics: [.combinedLocation]) + return context.account.postbox.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), anchor: .index(message.index), ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: inputTag, appendMessagesFromTheSameGroup: false, namespaces: namespaces, orderStatistics: [.combinedLocation]) |> mapToSignal { (view, _, _) -> Signal in let mapped = GalleryMessageHistoryView.view(view, peerIsCopyProtected) return .single(mapped) @@ -736,24 +790,25 @@ public class GalleryController: ViewController, StandalonePresentableController, let configuration = GalleryConfiguration.with(appConfiguration: appConfiguration) strongSelf.configuration = configuration - let entries = view.entries - var centralEntryStableId: UInt32? + let entries = galleryEntriesForMessageHistoryEntries(view.entries) + var centralEntryStableId: GalleryEntryStableId? loop: for i in 0 ..< entries.count { - let message = entries[i].message + let entry = entries[i] + let message = entry.entry.message switch source { case let .peerMessagesAtId(messageId, _, _, _): if message.id == messageId { - centralEntryStableId = message.stableId + centralEntryStableId = entry.stableId break loop } - case let .standaloneMessage(m): - if message.id == m.id { - centralEntryStableId = message.stableId + case let .standaloneMessage(m, mediaIndex): + if message.id == m.id && entry.mediaIndex == mediaIndex { + centralEntryStableId = entry.stableId break loop } case let .custom(_, messageId, _): if message.id == messageId { - centralEntryStableId = message.stableId + centralEntryStableId = entry.stableId break loop } } @@ -779,7 +834,7 @@ public class GalleryController: ViewController, StandalonePresentableController, var centralItemIndex: Int? for entry in strongSelf.entries { var isCentral = false - if entry.message.stableId == strongSelf.centralEntryStableId { + if entry.stableId == strongSelf.centralEntryStableId { isCentral = true } if let item = galleryItemForEntry(context: context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: streamSingleVideo, fromPlayingVideo: isCentral && fromPlayingVideo, landscape: isCentral && landscape, timecode: isCentral ? timecode : nil, playbackRate: { return self?.playbackRate }, displayInfoOnTop: displayInfoOnTop, configuration: configuration, translateToLanguage: translateToLanguage, peerIsCopyProtected: view.peerIsCopyProtected, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, generateStoreAfterDownload: strongSelf.generateStoreAfterDownload, present: { [weak self] c, a in @@ -1231,13 +1286,25 @@ public class GalleryController: ViewController, StandalonePresentableController, } if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments { - let message = self.entries[centralItemNode.index].message - if let (media, _) = mediaForMessage(message: message), let transitionArguments = presentationArguments.transitionArguments(message.id, media), !forceAway { - animatedOutNode = false - centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: { - animatedOutNode = true - completion() - }) + let entry = self.entries[centralItemNode.index] + let message = entry.entry.message + let media = mediaForMessage(message: message) + if !media.isEmpty { + var selectedMedia: Media? + if let mediaIndex = entry.mediaIndex { + if mediaIndex < media.count { + selectedMedia = media[Int(mediaIndex)].0 + } + } else if let media = media.first { + selectedMedia = media.0 + } + if let selectedMedia, let transitionArguments = presentationArguments.transitionArguments(message.id, selectedMedia), !forceAway { + animatedOutNode = false + centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: { + animatedOutNode = true + completion() + }) + } } } @@ -1291,9 +1358,22 @@ public class GalleryController: ViewController, StandalonePresentableController, self.galleryNode.transitionDataForCentralItem = { [weak self] in if let strongSelf = self { if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? GalleryControllerPresentationArguments { - let message = strongSelf.entries[centralItemNode.index].message - if let (media, _) = mediaForMessage(message: message), let transitionArguments = presentationArguments.transitionArguments(message.id, media) { - return (transitionArguments.transitionNode, transitionArguments.addToTransitionSurface) + let entry = strongSelf.entries[centralItemNode.index] + let message = entry.entry.message + let media = mediaForMessage(message: message) + if !media.isEmpty { + var selectedMedia: Media? + if let mediaIndex = entry.mediaIndex { + if mediaIndex < media.count { + selectedMedia = media[Int(mediaIndex)].0 + } + } else if let media = media.first { + selectedMedia = media.0 + } + + if let selectedMedia, let transitionArguments = presentationArguments.transitionArguments(message.id, selectedMedia) { + return (transitionArguments.transitionNode, transitionArguments.addToTransitionSurface) + } } } } @@ -1359,7 +1439,7 @@ public class GalleryController: ViewController, StandalonePresentableController, var centralItemIndex: Int? for entry in self.entries { var isCentral = false - if entry.message.stableId == self.centralEntryStableId { + if entry.stableId == self.centralEntryStableId { isCentral = true } if let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: entry, streamVideos: self.streamVideos, fromPlayingVideo: isCentral && self.fromPlayingVideo, landscape: isCentral && self.landscape, timecode: isCentral ? self.timecode : nil, playbackRate: { [weak self] in return self?.playbackRate }, displayInfoOnTop: displayInfoOnTop, configuration: self.configuration, peerIsCopyProtected: self.peerIsCopyProtected, performAction: self.performAction, openActionOptions: self.openActionOptions, storeMediaPlaybackState: self.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, generateStoreAfterDownload: self.generateStoreAfterDownload, present: { [weak self] c, a in @@ -1380,11 +1460,19 @@ public class GalleryController: ViewController, StandalonePresentableController, if let strongSelf = self { var hiddenItem: (MessageId, Media)? if let index = index { - let message = strongSelf.entries[index].message + let entry = strongSelf.entries[index] + let message = strongSelf.entries[index].entry.message - strongSelf.centralEntryStableId = message.stableId - if let (media, _) = mediaForMessage(message: message) { - hiddenItem = (message.id, media) + strongSelf.centralEntryStableId = entry.stableId + let media = mediaForMessage(message: message) + if !media.isEmpty { + if let mediaIndex = entry.mediaIndex { + if mediaIndex < media.count { + hiddenItem = (message.id, media[Int(mediaIndex)].0) + } + } else if let media = media.first { + hiddenItem = (message.id, media.0) + } } if let node = strongSelf.galleryNode.pager.centralItemNode() { @@ -1401,9 +1489,9 @@ public class GalleryController: ViewController, StandalonePresentableController, case let .peerMessagesAtId(_, chatLocation, _, chatLocationContextHolder): var reloadAroundIndex: MessageIndex? if index <= 2 && strongSelf.hasLeftEntries { - reloadAroundIndex = strongSelf.entries.first?.index + reloadAroundIndex = strongSelf.entries.first?.entry.index } else if index >= strongSelf.entries.count - 3 && strongSelf.hasRightEntries { - reloadAroundIndex = strongSelf.entries.last?.index + reloadAroundIndex = strongSelf.entries.last?.entry.index } let peerIsCopyProtected = strongSelf.peerIsCopyProtected if let reloadAroundIndex = reloadAroundIndex, let tag = strongSelf.tag { @@ -1415,7 +1503,7 @@ public class GalleryController: ViewController, StandalonePresentableController, } else { namespaces = .not(Namespaces.Message.allNonRegular) } - let signal = strongSelf.context.account.postbox.aroundMessageHistoryViewForLocation(strongSelf.context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), anchor: .index(reloadAroundIndex), ignoreMessagesInTimestampRange: nil, count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: tag, appendMessagesFromTheSameGroup: false, namespaces: namespaces, orderStatistics: [.combinedLocation]) + let signal = strongSelf.context.account.postbox.aroundMessageHistoryViewForLocation(strongSelf.context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), anchor: .index(reloadAroundIndex), ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: tag, appendMessagesFromTheSameGroup: false, namespaces: namespaces, orderStatistics: [.combinedLocation]) |> mapToSignal { (view, _, _) -> Signal in let mapped = GalleryMessageHistoryView.view(view, peerIsCopyProtected) return .single(mapped) @@ -1428,8 +1516,8 @@ public class GalleryController: ViewController, StandalonePresentableController, return } - let entries = view.entries - + let entries = galleryEntriesForMessageHistoryEntries(view.entries) + if strongSelf.invertItemOrder { strongSelf.entries = entries.reversed() strongSelf.hasLeftEntries = view.hasLater @@ -1444,7 +1532,7 @@ public class GalleryController: ViewController, StandalonePresentableController, var centralItemIndex: Int? for entry in strongSelf.entries { var isCentral = false - if entry.message.stableId == strongSelf.centralEntryStableId { + if entry.stableId == strongSelf.centralEntryStableId { isCentral = true } if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, playbackRate: { return self?.playbackRate }, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, peerIsCopyProtected: view.peerIsCopyProtected, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, generateStoreAfterDownload: strongSelf.generateStoreAfterDownload, present: { [weak self] c, a in @@ -1474,12 +1562,13 @@ public class GalleryController: ViewController, StandalonePresentableController, return } - var entries: [MessageHistoryEntry] = [] + var messageEntries: [MessageHistoryEntry] = [] var index = messages.count for message in messages.reversed() { - entries.append(MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false))) + messageEntries.append(MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false))) index -= 1 } + let entries = galleryEntriesForMessageHistoryEntries(messageEntries) if entries.count > strongSelf.entries.count { if strongSelf.invertItemOrder { @@ -1496,7 +1585,7 @@ public class GalleryController: ViewController, StandalonePresentableController, var centralItemIndex: Int? for entry in strongSelf.entries { var isCentral = false - if entry.message.stableId == strongSelf.centralEntryStableId { + if entry.stableId == strongSelf.centralEntryStableId { isCentral = true } if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, playbackRate: { return self?.playbackRate }, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, generateStoreAfterDownload: strongSelf.generateStoreAfterDownload, present: { [weak self] c, a in @@ -1555,7 +1644,8 @@ public class GalleryController: ViewController, StandalonePresentableController, var nodeAnimatesItself = false if let centralItemNode = self.galleryNode.pager.centralItemNode() { - let message = self.entries[centralItemNode.index].message + let entry = self.entries[centralItemNode.index] + self.centralItemTitle.set(centralItemNode.title()) self.centralItemTitleView.set(centralItemNode.titleView()) self.centralItemRightBarButtonItem.set(centralItemNode.rightBarButtonItem()) @@ -1563,17 +1653,30 @@ public class GalleryController: ViewController, StandalonePresentableController, self.centralItemNavigationStyle.set(centralItemNode.navigationStyle()) self.centralItemFooterContentNode.set(centralItemNode.footerContent()) self.galleryNode.pager.pagingEnabledPromise.set(centralItemNode.isPagingEnabled()) - - if let (media, _) = mediaForMessage(message: message) { - if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments, let transitionArguments = presentationArguments.transitionArguments(message.id, media) { - nodeAnimatesItself = true - if presentationArguments.animated { - centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {}) + + let message = entry.entry.message + let media = mediaForMessage(message: message) + if !media.isEmpty { + var selectedMedia: Media? + if let mediaIndex = entry.mediaIndex { + if mediaIndex < media.count { + selectedMedia = media[Int(mediaIndex)].0 } - - self._hiddenMedia.set(.single((message.id, media))) + } else if let media = media.first { + selectedMedia = media.0 + } + + if let selectedMedia { + if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments, let transitionArguments = presentationArguments.transitionArguments(message.id, selectedMedia) { + nodeAnimatesItself = true + if presentationArguments.animated { + centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {}) + } + + self._hiddenMedia.set(.single((message.id, selectedMedia))) + } + centralItemNode.activateAsInitial() } - centralItemNode.activateAsInitial() } self.onDidAppear?() @@ -1595,7 +1698,7 @@ public class GalleryController: ViewController, StandalonePresentableController, override public func didAppearInContextPreview() { if let centralItemNode = self.galleryNode.pager.centralItemNode() { - let message = self.entries[centralItemNode.index].message + let message = self.entries[centralItemNode.index].entry.message self.centralItemTitle.set(centralItemNode.title()) self.centralItemTitleView.set(centralItemNode.titleView()) self.centralItemRightBarButtonItem.set(centralItemNode.rightBarButtonItem()) @@ -1604,7 +1707,7 @@ public class GalleryController: ViewController, StandalonePresentableController, self.centralItemFooterContentNode.set(centralItemNode.footerContent()) self.galleryNode.pager.pagingEnabledPromise.set(centralItemNode.isPagingEnabled()) - if let _ = mediaForMessage(message: message) { + if !mediaForMessage(message: message).isEmpty { centralItemNode.activateAsInitial() } } diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index 48ca1b5f83b..5393185f882 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -228,15 +228,19 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture fromLeft = true } if let current = strongSelf.currentThumbnailContainerNode { + strongSelf.currentThumbnailContainerNode = nil if thumbnailContainerVisible { current.animateOut(toRight: fromLeft) current.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak current] _ in current?.removeFromSupernode() }) + if let (navigationHeight, layout) = strongSelf.containerLayout, node == nil { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) + } } } - strongSelf.currentThumbnailContainerNode = node if let node = node { + strongSelf.currentThumbnailContainerNode = node strongSelf.insertSubnode(node, aboveSubnode: strongSelf.footerNode) if let (navigationHeight, layout) = strongSelf.containerLayout, thumbnailContainerVisible { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) diff --git a/submodules/GalleryUI/Sources/GalleryFooterNode.swift b/submodules/GalleryUI/Sources/GalleryFooterNode.swift index 9c37116d6d7..673c4e5a31c 100644 --- a/submodules/GalleryUI/Sources/GalleryFooterNode.swift +++ b/submodules/GalleryUI/Sources/GalleryFooterNode.swift @@ -54,6 +54,8 @@ public final class GalleryFooterNode: ASDisplayNode { } self.addSubnode(footerContentNode) } + } else if let _ = self.currentThumbnailPanelHeight { + self.currentThumbnailPanelHeight = thumbnailPanelHeight } var animateOverlayIn = false diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 03e7d7c4ff6..9c39a5d896f 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -91,6 +91,8 @@ final class ChatMediaGalleryThumbnailItem: GalleryThumbnailItem { case let .video(fileReference): if let representation = largestImageRepresentation(fileReference.media.previewRepresentations) { return (mediaGridMessageVideo(postbox: self.account.postbox, userLocation: self.userLocation, videoReference: fileReference), representation.dimensions.cgSize) + } else if let dimensions = fileReference.media.dimensions { + return (mediaGridMessageVideo(postbox: self.account.postbox, userLocation: self.userLocation, videoReference: fileReference), dimensions.cgSize) } else { return (.single({ _ in return nil }), CGSize(width: 128.0, height: 128.0)) } @@ -112,6 +114,7 @@ class ChatImageGalleryItem: GalleryItem { let context: AccountContext let presentationData: PresentationData let message: Message + let mediaIndex: Int? let location: MessageHistoryEntryLocation? let translateToLanguage: String? let peerIsCopyProtected: Bool @@ -121,10 +124,11 @@ class ChatImageGalleryItem: GalleryItem { let openActionOptions: (GalleryControllerInteractionTapAction, Message) -> Void let present: (ViewController, Any?) -> Void - init(context: AccountContext, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false, isSecret: Bool = false, displayInfoOnTop: Bool, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) { + init(context: AccountContext, presentationData: PresentationData, message: Message, mediaIndex: Int? = nil, location: MessageHistoryEntryLocation?, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false, isSecret: Bool = false, displayInfoOnTop: Bool, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.presentationData = presentationData self.message = message + self.mediaIndex = mediaIndex self.location = location self.translateToLanguage = translateToLanguage self.peerIsCopyProtected = peerIsCopyProtected @@ -140,7 +144,12 @@ class ChatImageGalleryItem: GalleryItem { node.setMessage(self.message, displayInfo: !self.displayInfoOnTop, translateToLanguage: self.translateToLanguage, peerIsCopyProtected: self.peerIsCopyProtected, isSecret: self.isSecret) for media in self.message.media { - if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia, let image = fullMedia as? TelegramMediaImage { + if let paidContent = media as? TelegramMediaPaidContent { + let mediaIndex = self.mediaIndex ?? 0 + if case let .full(fullMedia) = paidContent.extendedMedia[Int(mediaIndex)], let image = fullMedia as? TelegramMediaImage { + node.setImage(userLocation: .peer(self.message.id.peerId), imageReference: .message(message: MessageReference(self.message), media: image)) + } + } else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia, let image = fullMedia as? TelegramMediaImage { node.setImage(userLocation: .peer(self.message.id.peerId), imageReference: .message(message: MessageReference(self.message), media: image)) } else if let image = media as? TelegramMediaImage { node.setImage(userLocation: .peer(self.message.id.peerId), imageReference: .message(message: MessageReference(self.message), media: image)) @@ -182,7 +191,18 @@ class ChatImageGalleryItem: GalleryItem { } func thumbnailItem() -> (Int64, GalleryThumbnailItem)? { - if let id = self.message.groupInfo?.stableId { + if let paidContent = self.message.paidContent { + var mediaReference: AnyMediaReference? + let mediaIndex = self.mediaIndex ?? 0 + if case let .full(fullMedia) = paidContent.extendedMedia[Int(mediaIndex)], let m = fullMedia as? TelegramMediaImage { + mediaReference = .message(message: MessageReference(self.message), media: m) + } + if let mediaReference = mediaReference { + if let item = ChatMediaGalleryThumbnailItem(account: self.context.account, userLocation: .peer(self.message.id.peerId), mediaReference: mediaReference) { + return (0, item) + } + } + } else if let id = self.message.groupInfo?.stableId { var mediaReference: AnyMediaReference? for m in self.message.media { if let m = m as? TelegramMediaImage { @@ -238,6 +258,16 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { private var currentSpeechHolder: SpeechSynthesizerHolder? + override var baseNavigationController: () -> NavigationController? { + didSet { + if let _ = self.baseNavigationController() { + self.moreBarButton.isHidden = false + } else { + self.moreBarButton.isHidden = true + } + } + } + init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.presentationData = presentationData @@ -336,7 +366,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self.translateToLanguage = translateToLanguage self.peerIsCopyProtected = peerIsCopyProtected self.isSecret = isSecret - self.imageNode.captureProtected = message.id.peerId.namespace == Namespaces.Peer.SecretChat || message.isCopyProtected() || peerIsCopyProtected || isSecret + self.imageNode.captureProtected = message.id.peerId.namespace == Namespaces.Peer.SecretChat || message.isCopyProtected() || peerIsCopyProtected || isSecret || message.paidContent != nil self.footerContentNode.setMessage(message, displayInfo: displayInfo, translateToLanguage: translateToLanguage, peerIsCopyProtected: peerIsCopyProtected) } @@ -356,7 +386,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.statusNodeContainer.isHidden = true Queue.concurrentDefaultQueue().async { - if let message = strongSelf.message, !message.isCopyProtected() && !imageReference.media.flags.contains(.hasStickers) { + if let message = strongSelf.message, !message.isCopyProtected() && !imageReference.media.flags.contains(.hasStickers) && message.paidContent == nil { strongSelf.recognitionDisposable.set((recognizedContent(context: strongSelf.context, image: { return generate(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))?.generateImage() }, messageId: message.id) |> deliverOnMainQueue).start(next: { [weak self] results in if let strongSelf = self { @@ -526,7 +556,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { }) }))) - if !message.isCopyProtected() && !self.peerIsCopyProtected, let media = self.contextAndMedia?.1 { + if !message.isCopyProtected() && !self.peerIsCopyProtected && message.paidContent == nil, let media = self.contextAndMedia?.1 { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Gallery_SaveImage, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.default) diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 204ef30309c..e7409fa6087 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -25,7 +25,7 @@ import TextFormat import SliderContextItem public enum UniversalVideoGalleryItemContentInfo { - case message(Message) + case message(Message, Int?) case webPage(TelegramMediaWebpage, Media, ((@escaping () -> GalleryTransitionArguments?, NavigationController?, (ViewController, Any?) -> Void) -> Void)?) } @@ -93,7 +93,7 @@ public class UniversalVideoGalleryItem: GalleryItem { node.setupItem(self) - if self.displayInfoOnTop, case let .message(message) = self.contentInfo { + if self.displayInfoOnTop, case let .message(message, _) = self.contentInfo { node.titleContentView?.setMessage(message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId) } @@ -108,7 +108,7 @@ public class UniversalVideoGalleryItem: GalleryItem { node.setupItem(self) - if self.displayInfoOnTop, case let .message(message) = self.contentInfo { + if self.displayInfoOnTop, case let .message(message, _) = self.contentInfo { node.titleContentView?.setMessage(message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId) } } @@ -118,8 +118,19 @@ public class UniversalVideoGalleryItem: GalleryItem { guard let contentInfo = self.contentInfo else { return nil } - if case let .message(message) = contentInfo { - if let id = message.groupInfo?.stableId { + if case let .message(message, mediaIndex) = contentInfo { + if let paidContent = message.paidContent { + var mediaReference: AnyMediaReference? + let mediaIndex = mediaIndex ?? 0 + if case let .full(fullMedia) = paidContent.extendedMedia[Int(mediaIndex)], let m = fullMedia as? TelegramMediaFile { + mediaReference = .message(message: MessageReference(message), media: m) + } + if let mediaReference = mediaReference { + if let item = ChatMediaGalleryThumbnailItem(account: self.context.account, userLocation: .peer(message.id.peerId), mediaReference: mediaReference) { + return (0, item) + } + } + } else if let id = message.groupInfo?.stableId { var mediaReference: AnyMediaReference? for m in message.media { if let m = m as? TelegramMediaImage { @@ -1183,8 +1194,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var mediaFileStatus: Signal = .single(nil) var hintSeekable = false - if let contentInfo = item.contentInfo, case let .message(message) = contentInfo { - if Namespaces.Message.allNonRegular.contains(message.id.namespace) { + if let contentInfo = item.contentInfo, case let .message(message, _) = contentInfo { + if message.paidContent != nil { + disablePictureInPicture = true + } else if Namespaces.Message.allNonRegular.contains(message.id.namespace) || message.id.namespace == Namespaces.Message.Local { disablePictureInPicture = true } else { let throttledSignal = videoNode.status @@ -1432,7 +1445,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.hasPictureInPicture = false } - if let contentInfo = item.contentInfo, case let .message(message) = contentInfo { + if let contentInfo = item.contentInfo, case let .message(message, mediaIndex) = contentInfo { var file: TelegramMediaFile? for m in message.media { if let m = m as? TelegramMediaFile, m.isVideo { @@ -1441,6 +1454,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } else if let m = m as? TelegramMediaWebpage, case let .Loaded(content) = m.content, let f = content.file, f.isVideo { file = f break + } else if let paidContent = message.paidContent { + let mediaIndex = mediaIndex ?? 0 + let media = paidContent.extendedMedia[mediaIndex] + if case let .full(fullMedia) = media, let m = fullMedia as? TelegramMediaFile { + file = m + } + break } } @@ -1450,6 +1470,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } else if let file = file, !file.isAnimated { hasMoreButton = true } + + if let _ = message.paidContent, message.id.namespace == Namespaces.Message.Local { + hasMoreButton = false + } if hasMoreButton { let moreMenuItem = UIBarButtonItem(customDisplayNode: self.moreBarButton)! @@ -1506,7 +1530,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if let contentInfo = item.contentInfo { switch contentInfo { - case let .message(message): + case let .message(message, _): self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected) case let .webPage(webPage, media, _): self.footerContentNode.setWebPage(webPage, media: media) @@ -1558,7 +1582,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { isLocal = true } var isStreamable = false - if let contentInfo = item.contentInfo, case let .message(message) = contentInfo { + if let contentInfo = item.contentInfo, case let .message(message, _) = contentInfo { isStreamable = isMediaStreamable(message: message, media: content.fileReference.media) } else { isStreamable = isMediaStreamable(media: content.fileReference.media) @@ -2131,7 +2155,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } switch contentInfo { - case let .message(message): + case let .message(message, _): let gallery = GalleryController(context: context, source: .peerMessagesAtId(messageId: message.id, chatLocation: .peer(id: message.id.peerId), customTag: nil, chatLocationContextHolder: Atomic(value: nil)), playbackRate: playbackRate, replaceRootController: { controller, ready in if let baseNavigationController = baseNavigationController { baseNavigationController.replaceTopController(controller, animated: false, ready: ready) @@ -2197,7 +2221,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { @objc func pictureInPictureButtonPressed() { var isNativePictureInPictureSupported = false switch self.item?.contentInfo { - case let .message(message): + case let .message(message, _): for media in message.media { if let media = media as? TelegramMediaFile, media.isVideo { if message.id.namespace == Namespaces.Message.Cloud { @@ -2227,7 +2251,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var hiddenMedia: (MessageId, Media)? = nil switch item.contentInfo { - case let .message(message): + case let .message(message, _): for media in message.media { if let media = media as? TelegramMediaImage { hiddenMedia = (message.id, media) @@ -2255,7 +2279,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } switch contentInfo { - case let .message(message): + case let .message(message, _): let gallery = GalleryController(context: context, source: .peerMessagesAtId(messageId: message.id, chatLocation: .peer(id: message.id.peerId), customTag: nil, chatLocationContextHolder: Atomic(value: nil)), playbackRate: playbackRate, replaceRootController: { [weak baseNavigationController] controller, ready in if let baseNavigationController = baseNavigationController { baseNavigationController.replaceTopController(controller, animated: false, ready: ready) @@ -2288,7 +2312,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var expandImpl: (() -> Void)? let shouldBeDismissed: Signal - if let contentInfo = item.contentInfo, case let .message(message) = contentInfo { + if let contentInfo = item.contentInfo, case let .message(message, _) = contentInfo { shouldBeDismissed = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: message.id)) |> map { message -> Bool in if let _ = message { @@ -2316,8 +2340,15 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } switch contentInfo { - case let .message(message): - let gallery = GalleryController(context: context, source: .peerMessagesAtId(messageId: message.id, chatLocation: .peer(id: message.id.peerId), customTag: nil, chatLocationContextHolder: Atomic(value: nil)), playbackRate: playbackRate, replaceRootController: { controller, ready in + case let .message(message, messageIndex): + let source: GalleryControllerItemSource + if let _ = message.paidContent { + source = .standaloneMessage(message, messageIndex) + } else { + source = .peerMessagesAtId(messageId: message.id, chatLocation: .peer(id: message.id.peerId), customTag: nil, chatLocationContextHolder: Atomic(value: nil)) + } + + let gallery = GalleryController(context: context, source: source, playbackRate: playbackRate, replaceRootController: { controller, ready in if let baseNavigationController = baseNavigationController { baseNavigationController.replaceTopController(controller, animated: false, ready: ready) } @@ -2385,11 +2416,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { guard let item = self.item else { return nil } - if let contentInfo = item.contentInfo, case let .message(message) = contentInfo { + if let contentInfo = item.contentInfo, case let .message(message, mediaIndex) = contentInfo { var file: TelegramMediaFile? var isWebpage = false for m in message.media { - if let m = m as? TelegramMediaFile, m.isVideo { + if let paidContent = m as? TelegramMediaPaidContent { + let media = paidContent.extendedMedia[mediaIndex ?? 0] + if case let .full(fullMedia) = media, let fullMedia = fullMedia as? TelegramMediaFile, fullMedia.isVideo { + file = fullMedia + } + break + } else if let m = m as? TelegramMediaFile, m.isVideo { file = m break } else if let m = m as? TelegramMediaWebpage, case let .Loaded(content) = m.content { @@ -2573,7 +2610,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected { + if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveVideo, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.default) diff --git a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift index 911712c5059..0360571fe4e 100644 --- a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift +++ b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift @@ -525,7 +525,8 @@ public final class SecretMediaPreviewController: ViewController { } } - guard let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)), streamVideos: false, hideControls: true, isSecret: true, playbackRate: { nil }, peerIsCopyProtected: true, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in + let entry = GalleryEntry(entry: MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false))) + guard let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: entry, streamVideos: false, hideControls: true, isSecret: true, playbackRate: { nil }, peerIsCopyProtected: true, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in if let self { if self.currentNodeMessageIsViewOnce || (duration < 30.0 && !self.currentMessageIsDismissed) { if let node = self.controllerNode.pager.centralItemNode() as? UniversalVideoGalleryItemNode { diff --git a/submodules/Geocoding/Sources/Geocoding.swift b/submodules/Geocoding/Sources/Geocoding.swift index c0e43e749d6..ce8bb7f02f0 100644 --- a/submodules/Geocoding/Sources/Geocoding.swift +++ b/submodules/Geocoding/Sources/Geocoding.swift @@ -38,6 +38,7 @@ public struct ReverseGeocodedPlacemark { public let name: String? public let street: String? public let city: String? + public let state: String? public let country: String? public let countryCode: String? @@ -79,12 +80,12 @@ public func reverseGeocodeLocation(latitude: Double, longitude: Double, locale: let countryCode = placemark.isoCountryCode let result: ReverseGeocodedPlacemark if placemark.thoroughfare == nil && placemark.locality == nil && placemark.country == nil { - result = ReverseGeocodedPlacemark(name: placemark.name, street: placemark.name, city: nil, country: nil, countryCode: nil) + result = ReverseGeocodedPlacemark(name: placemark.name, street: placemark.name, city: nil, state: nil, country: nil, countryCode: nil) } else { if placemark.thoroughfare == nil && placemark.locality == nil, let ocean = placemark.ocean { - result = ReverseGeocodedPlacemark(name: ocean, street: nil, city: nil, country: countryName, countryCode: countryCode) + result = ReverseGeocodedPlacemark(name: ocean, street: nil, city: nil, state: nil, country: countryName, countryCode: countryCode) } else { - result = ReverseGeocodedPlacemark(name: nil, street: placemark.thoroughfare, city: placemark.locality, country: countryName, countryCode: countryCode) + result = ReverseGeocodedPlacemark(name: nil, street: placemark.thoroughfare, city: placemark.locality, state: placemark.administrativeArea, country: countryName, countryCode: countryCode) } } subscriber.putNext(result) diff --git a/submodules/GraphCore/Sources/Charts/Controllers/BaseChartController.swift b/submodules/GraphCore/Sources/Charts/Controllers/BaseChartController.swift index f24cbb03252..8ecceb12edc 100644 --- a/submodules/GraphCore/Sources/Charts/Controllers/BaseChartController.swift +++ b/submodules/GraphCore/Sources/Charts/Controllers/BaseChartController.swift @@ -72,6 +72,18 @@ enum BaseConstants { return numberFormatter }() + static let starNumberFormatter: NumberFormatter = { + let numberFormatter = NumberFormatter() + numberFormatter.allowsFloats = true + numberFormatter.numberStyle = .decimal + numberFormatter.usesGroupingSeparator = true + numberFormatter.groupingSeparator = " " + numberFormatter.minimumIntegerDigits = 1 + numberFormatter.minimumFractionDigits = 0 + numberFormatter.maximumFractionDigits = 2 + return numberFormatter + }() + static let detailsNumberFormatter: NumberFormatter = { let detailsNumberFormatter = NumberFormatter() detailsNumberFormatter.allowsFloats = false diff --git a/submodules/GraphCore/Sources/Charts/Controllers/Lines/GeneralLinesChartController.swift b/submodules/GraphCore/Sources/Charts/Controllers/Lines/GeneralLinesChartController.swift index 86762713151..4da43fc1a05 100644 --- a/submodules/GraphCore/Sources/Charts/Controllers/Lines/GeneralLinesChartController.swift +++ b/submodules/GraphCore/Sources/Charts/Controllers/Lines/GeneralLinesChartController.swift @@ -33,7 +33,7 @@ public class GeneralLinesChartController: BaseLinesChartController { private var prevoiusHorizontalStrideInterval: Int = 1 - private (set) var chartLines: [LinesChartRenderer.LineData] = [] + private(set) var chartLines: [LinesChartRenderer.LineData] = [] override public init(chartsCollection: ChartsCollection) { self.initialChartCollection = chartsCollection diff --git a/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift b/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift index 227bb0d9d91..cf9bd1260a7 100644 --- a/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift +++ b/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift @@ -25,7 +25,7 @@ class BarsComponentController: GeneralChartComponentController { let previewBarsChartRenderer: BarChartRenderer private(set) var barsWidth: CGFloat = 1 - private (set) var chartBars: BarChartRenderer.BarsData = .blank + private(set) var chartBars: BarChartRenderer.BarsData = .blank private var step: Bool diff --git a/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/StackedBarsChartController.swift b/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/StackedBarsChartController.swift index 0ac4feb20de..613e946a42e 100644 --- a/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/StackedBarsChartController.swift +++ b/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/StackedBarsChartController.swift @@ -13,7 +13,23 @@ import Cocoa import UIKit #endif +public enum GraphCurrency : String { + case xtr = "XTR" + case ton = "TON" + + var formatter: NumberFormatter { + switch self { + case .xtr: + return BaseConstants.starNumberFormatter + case .ton: + return BaseConstants.tonNumberFormatter + } + } +} + public class StackedBarsChartController: BaseChartController { + + let barsController: BarsComponentController let zoomedBarsController: BarsComponentController @@ -23,12 +39,12 @@ public class StackedBarsChartController: BaseChartController { } } - public init(chartsCollection: ChartsCollection, isCrypto: Bool = false, rate: Double = 1.0) { + public init(chartsCollection: ChartsCollection, currency: GraphCurrency? = nil, drawCurrency:((CGContext, UIColor, CGPoint)->Void)? = nil, rate: Double = 1.0) { let horizontalScalesRenderer = HorizontalScalesRenderer() let verticalScalesRenderer = VerticalScalesRenderer() var secondaryScalesRenderer: VerticalScalesRenderer? - if isCrypto { - verticalScalesRenderer.isCrypto = true + if let _ = currency { + verticalScalesRenderer.drawCurrency = drawCurrency secondaryScalesRenderer = VerticalScalesRenderer() secondaryScalesRenderer?.isRightAligned = true } @@ -38,10 +54,10 @@ public class StackedBarsChartController: BaseChartController { verticalScalesRenderer: verticalScalesRenderer, secondaryScalesRenderer: secondaryScalesRenderer, previewBarsChartRenderer: BarChartRenderer()) - if isCrypto { + if let currency { barsController.conversionRate = rate - barsController.verticalLimitsNumberFormatter = BaseConstants.tonNumberFormatter - barsController.detailsNumberFormatter = BaseConstants.tonNumberFormatter + barsController.verticalLimitsNumberFormatter = currency.formatter + barsController.detailsNumberFormatter = currency.formatter } zoomedBarsController = BarsComponentController(isZoomed: true, mainBarsRenderer: BarChartRenderer(), diff --git a/submodules/GraphCore/Sources/Charts/Renderes/VerticalScalesRenderer.swift b/submodules/GraphCore/Sources/Charts/Renderes/VerticalScalesRenderer.swift index 2245bd62b35..fa11122d141 100644 --- a/submodules/GraphCore/Sources/Charts/Renderes/VerticalScalesRenderer.swift +++ b/submodules/GraphCore/Sources/Charts/Renderes/VerticalScalesRenderer.swift @@ -9,7 +9,7 @@ import Foundation #if os(macOS) import Cocoa -typealias UIColor = NSColor +public typealias UIColor = NSColor #else import UIKit #endif @@ -26,7 +26,7 @@ class VerticalScalesRenderer: BaseChartRenderer { var axisXWidth: CGFloat = GView.oneDevicePixel var isRightAligned: Bool = false - var isCrypto: Bool = false + var drawCurrency:((CGContext, UIColor, CGPoint)->Void)? var horizontalLinesColor: GColor = .black { didSet { @@ -122,45 +122,6 @@ class VerticalScalesRenderer: BaseChartRenderer { context.strokeLineSegments(between: lineSegments) } - func drawTonSymbol(context: CGContext, color: UIColor, at point: CGPoint) { - let width: CGFloat = 8.0 - let height: CGFloat = 7.5 - let cornerRadius: CGFloat = 0.5 - - let topPoint = CGPoint(x: point.x + width / 2, y: point.y) - let bottomPoint = CGPoint(x: point.x + width / 2, y: point.y + height) - let leftTopPoint = CGPoint(x: point.x, y: point.y) - let rightTopPoint = CGPoint(x: point.x + width, y: point.y) - - context.saveGState() - - context.beginPath() - context.move(to: CGPoint(x: leftTopPoint.x + cornerRadius, y: leftTopPoint.y)) - - context.addArc(tangent1End: leftTopPoint, tangent2End: bottomPoint, radius: cornerRadius) - context.addLine(to: CGPoint(x: bottomPoint.x, y: bottomPoint.y - cornerRadius + GView.oneDevicePixel)) - - context.move(to: CGPoint(x: rightTopPoint.x - cornerRadius, y: rightTopPoint.y)) - context.addArc(tangent1End: rightTopPoint, tangent2End: bottomPoint, radius: cornerRadius) - context.addLine(to: CGPoint(x: bottomPoint.x, y: bottomPoint.y - cornerRadius + GView.oneDevicePixel)) - - context.move(to: CGPoint(x: leftTopPoint.x + cornerRadius, y: leftTopPoint.y)) - context.addLine(to: CGPoint(x: rightTopPoint.x - cornerRadius, y: rightTopPoint.y)) - - context.move(to: topPoint) - context.addLine(to: CGPoint(x: bottomPoint.x, y: bottomPoint.y - 1.0)) - - context.setLineWidth(1.0) - context.setLineCap(.round) - context.setFillColor(UIColor.clear.cgColor) - context.setStrokeColor(color.withAlphaComponent(1.0).cgColor) - - context.setAlpha(color.alphaValue) - context.strokePath() - - context.restoreGState() - } - func drawVerticalLabels(_ labels: [LinesChartLabel], attributes: [NSAttributedString.Key: Any]) { if isRightAligned { for label in labels { @@ -176,9 +137,9 @@ class VerticalScalesRenderer: BaseChartRenderer { let textNode = LabelNode.layoutText(attributedString, bounds.size) var xOffset = 0.0 - if self.isCrypto { + if let drawCurrency { xOffset += 11.0 - drawTonSymbol(context: context, color: attributes[.foregroundColor] as? UIColor ?? .black, at: CGPoint(x: chartFrame.minX, y: y + 4.0)) + drawCurrency(context, attributes[.foregroundColor] as? UIColor ?? .black, CGPoint(x: chartFrame.minX, y: y + 4.0)) } textNode.1.draw(CGRect(origin: CGPoint(x: chartFrame.minX + xOffset, y: y), size: textNode.0.size), in: context, backingScaleFactor: deviceScale) diff --git a/submodules/GraphCore/Sources/Helpers/ScalesNumberFormatter.swift b/submodules/GraphCore/Sources/Helpers/ScalesNumberFormatter.swift index fa25137fd76..19f18334aaa 100644 --- a/submodules/GraphCore/Sources/Helpers/ScalesNumberFormatter.swift +++ b/submodules/GraphCore/Sources/Helpers/ScalesNumberFormatter.swift @@ -16,7 +16,7 @@ import UIKit private let milionsScale = "M" private let thousandsScale = "K" -class ScalesNumberFormatter: NumberFormatter { +class ScalesNumberFormatter: NumberFormatter, @unchecked Sendable { override func string(from number: NSNumber) -> String? { let value = number.doubleValue let pow = log10(value) @@ -36,7 +36,7 @@ class ScalesNumberFormatter: NumberFormatter { } } -class TonNumberFormatter: NumberFormatter { +class TonNumberFormatter: NumberFormatter, @unchecked Sendable { override func string(from number: NSNumber) -> String? { var balanceText = "\(number.intValue)" let decimalSeparator = self.decimalSeparator ?? "." @@ -60,3 +60,5 @@ class TonNumberFormatter: NumberFormatter { return balanceText } } + + diff --git a/submodules/GraphCore/Sources/Helpers/UIColor+Utils.swift b/submodules/GraphCore/Sources/Helpers/UIColor+Utils.swift index fde53f46be1..a25a87b2c88 100644 --- a/submodules/GraphCore/Sources/Helpers/UIColor+Utils.swift +++ b/submodules/GraphCore/Sources/Helpers/UIColor+Utils.swift @@ -21,7 +21,7 @@ func makeCIColor(color: GColor) -> CIColor { #endif } -extension GColor { +public extension GColor { var redValue: CGFloat{ return makeCIColor(color: self).red } var greenValue: CGFloat{ return makeCIColor(color: self).green } var blueValue: CGFloat{ return makeCIColor(color: self).blue } diff --git a/submodules/GraphCore/Sources/Helpers/UIView+Extensions.swift b/submodules/GraphCore/Sources/Helpers/UIView+Extensions.swift index ef76b00fcb5..ccc57a12585 100644 --- a/submodules/GraphCore/Sources/Helpers/UIView+Extensions.swift +++ b/submodules/GraphCore/Sources/Helpers/UIView+Extensions.swift @@ -20,6 +20,6 @@ public typealias GView = UIView #endif -extension GView { +public extension GView { static let oneDevicePixel: CGFloat = (1.0 / max(2, min(1, deviceScale))) } diff --git a/submodules/GraphUI/Sources/ChartNode.swift b/submodules/GraphUI/Sources/ChartNode.swift index ae351ac01ad..6391759d9f4 100644 --- a/submodules/GraphUI/Sources/ChartNode.swift +++ b/submodules/GraphUI/Sources/ChartNode.swift @@ -19,6 +19,7 @@ public enum ChartType { case twoAxisHourlyStep case twoAxis5MinStep case currency + case stars } public extension ChartTheme { @@ -87,7 +88,42 @@ public func createChartController(_ data: String, type: ChartType, rate: Double controller = StackedBarsChartController(chartsCollection: collection) controller.isZoomable = false case .currency: - controller = StackedBarsChartController(chartsCollection: collection, isCrypto: true, rate: rate) + var iconCache: [UInt32: UIImage] = [:] + controller = StackedBarsChartController(chartsCollection: collection, currency: .ton, drawCurrency: { context, color, point in + let icon: UIImage? + if let current = iconCache[color.rgb] { + icon = current + } else if let image = generateTintedImage(image: UIImage(bundleImageName: "Ads/Ton"), color: color) { + icon = generateImage(image.size, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + if let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false) + } + }) + iconCache[color.rgb] = icon + } else { + icon = nil + } + if let icon, let cgImage = icon.cgImage { + context.draw(cgImage, in: CGRect(origin: point.offsetBy(dx: 0.0, dy: -2.0), size: icon.size), byTiling: false) + } + }, rate: rate) + controller.isZoomable = false + case .stars: + var icon: UIImage? + if let image = UIImage(bundleImageName: "Premium/Stars/StarSmall") { + icon = generateImage(CGSize(width: floor(image.size.width * 0.82), height: floor(image.size.width * 0.82)), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + if let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false) + } + }) + } + controller = StackedBarsChartController(chartsCollection: collection, currency: .xtr, drawCurrency: { context, color, point in + if let icon, let cgImage = icon.cgImage { + context.draw(cgImage, in: CGRect(origin: point.offsetBy(dx: -3.0, dy: -4.0), size: icon.size), byTiling: false) + } + }, rate: rate) controller.isZoomable = false case .step: controller = StepBarsChartController(chartsCollection: collection) diff --git a/submodules/GraphUI/Sources/ChartVisibilityView.swift b/submodules/GraphUI/Sources/ChartVisibilityView.swift index fc110df0fab..30927d7803a 100644 --- a/submodules/GraphUI/Sources/ChartVisibilityView.swift +++ b/submodules/GraphUI/Sources/ChartVisibilityView.swift @@ -94,7 +94,7 @@ class ChartVisibilityView: UIView { } } - private (set) var selectedItems: [Bool] = [] + private(set) var selectedItems: [Bool] = [] var isExpanded: Bool = true { didSet { invalidateIntrinsicContentSize() diff --git a/submodules/HashtagSearchUI/BUILD b/submodules/HashtagSearchUI/BUILD index 39640b2499a..9c8735eeba4 100644 --- a/submodules/HashtagSearchUI/BUILD +++ b/submodules/HashtagSearchUI/BUILD @@ -24,6 +24,10 @@ swift_library( "//submodules/Postbox:Postbox", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/ComponentFlow", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 9e2496cca91..dfdd9fbaef8 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -23,7 +23,7 @@ public final class HashtagSearchController: TelegramBaseController { private var transitionDisposable: Disposable? private let openMessageFromSearchDisposable = MetaDisposable() - private var presentationData: PresentationData + private(set) var presentationData: PresentationData private var presentationDataDisposable: Disposable? private let animationCache: AnimationCache @@ -54,14 +54,17 @@ public final class HashtagSearchController: TelegramBaseController { self.presentationDataDisposable = (self.context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - let previousTheme = strongSelf.presentationData.theme - let previousStrings = strongSelf.presentationData.strings + if let self { + let previousTheme = self.presentationData.theme + let previousStrings = self.presentationData.strings - strongSelf.presentationData = presentationData + self.presentationData = presentationData if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { - strongSelf.updateThemeAndStrings() + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + + self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) + self.controllerNode.updatePresentationData(self.presentationData) } } }) @@ -88,15 +91,7 @@ public final class HashtagSearchController: TelegramBaseController { self.displayNodeDidLoad() } - - private func updateThemeAndStrings() { - self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - - self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) - self.controllerNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) - } - public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift index 6e7883caa79..3ef278aa27f 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift @@ -1,6 +1,7 @@ import Display import UIKit import AsyncDisplayKit +import ComponentFlow import SwiftSignalKit import TelegramCore import TelegramPresentationData @@ -13,6 +14,8 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg private let context: AccountContext private weak var controller: HashtagSearchController? private var query: String + private var isCashtag = false + private var presentationData: PresentationData private let searchQueryPromise = ValuePromise() private var searchQueryDisposable: Disposable? @@ -35,6 +38,11 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg let globalController: ChatController? let globalChatContents: HashtagSearchGlobalChatContents? + private var globalStorySearchContext: SearchStoryListContext? + private var globalStorySearchDisposable = MetaDisposable() + private var globalStorySearchState: StoryListContext.State? + private var globalStorySearchComponentView: ComponentView? + private var panRecognizer: InteractiveTransitionGestureRecognizer? private var containerLayout: (ContainerViewLayout, CGFloat)? @@ -45,6 +53,8 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.controller = controller self.query = query self.navigationBar = navigationBar + self.isCashtag = query.hasPrefix("$") + self.presentationData = controller.presentationData let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -53,8 +63,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.containerNode = ASDisplayNode() - let cleanHashtag = cleanHashtag(query) - self.searchContentNode = HashtagSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, initialQuery: cleanHashtag, hasCurrentChat: peer != nil, cancel: { [weak controller] in + self.searchContentNode = HashtagSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, initialQuery: query, hasCurrentChat: peer != nil, cancel: { [weak controller] in controller?.dismiss() }) @@ -67,7 +76,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg let navigationController = controller.navigationController as? NavigationController if let peer, !controller.all { - self.currentController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .inline(navigationController)) + self.currentController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .inline(navigationController), params: nil) self.currentController?.alwaysShowSearchResultsAsList = true self.currentController?.showListEmptyResults = true self.currentController?.customNavigationController = navigationController @@ -75,20 +84,20 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.currentController = nil } - let myChatContents = HashtagSearchGlobalChatContents(context: context, query: cleanHashtag, publicPosts: false) + let myChatContents = HashtagSearchGlobalChatContents(context: context, query: query, publicPosts: false) self.myChatContents = myChatContents - self.myController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: myChatContents), botStart: nil, mode: .standard(.default)) + self.myController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: myChatContents), botStart: nil, mode: .standard(.default), params: nil) self.myController?.alwaysShowSearchResultsAsList = true self.myController?.showListEmptyResults = true self.myController?.customNavigationController = navigationController - let globalChatContents = HashtagSearchGlobalChatContents(context: context, query: cleanHashtag, publicPosts: true) + let globalChatContents = HashtagSearchGlobalChatContents(context: context, query: query, publicPosts: true) self.globalChatContents = globalChatContents - self.globalController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: globalChatContents), botStart: nil, mode: .standard(.default)) + self.globalController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: globalChatContents), botStart: nil, mode: .standard(.default), params: nil) self.globalController?.alwaysShowSearchResultsAsList = true self.globalController?.showListEmptyResults = true self.globalController?.customNavigationController = navigationController - + if controller.publicPosts { self.searchContentNode.selectedIndex = 2 } else if peer == nil { @@ -140,9 +149,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } else if index == 2 { self.isSearching.set(self.globalChatContents?.searching ?? .single(false)) } - if let (layout, navigationHeight) = self.containerLayout { - let _ = self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) - } + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) } self.recentListNode.setSearchQuery = { [weak self] query in @@ -172,9 +179,11 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self?.searchQueryPromise.set(query) } - let _ = addRecentHashtagSearchQuery(engine: context.engine, string: cleanHashtag).startStandalone() - self.searchContentNode.onReturn = { query in + if !self.isCashtag { let _ = addRecentHashtagSearchQuery(engine: context.engine, string: query).startStandalone() + self.searchContentNode.onReturn = { query in + let _ = addRecentHashtagSearchQuery(engine: context.engine, string: "#" + query).startStandalone() + } } let throttledSearchQuery = self.searchQueryPromise.get() @@ -190,7 +199,13 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.searchQueryDisposable = (throttledSearchQuery |> deliverOnMainQueue).start(next: { [weak self] query in if let self { - self.updateSearchQuery(query) + let prefix: String + if self.isCashtag { + prefix = "$" + } else { + prefix = "#" + } + self.updateSearchQuery(prefix + query) } }) @@ -202,11 +217,14 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg transition.updateAlpha(node: self.shimmerNode, alpha: isSearching ? 1.0 : 0.0) } }) + + self.updateStorySearch() } deinit { self.searchQueryDisposable?.dispose() self.isSearchingDisposable?.dispose() + self.globalStorySearchDisposable.dispose() } private var panAllowedDirections: InteractiveTransitionGestureRecognizerDirections { @@ -278,9 +296,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.searchContentNode.transitionFraction = self.panTransitionFraction - if let (layout, navigationHeight) = self.containerLayout { - let _ = self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) - } + self.requestUpdate(transition: .immediate) case .ended, .cancelled: var directionIsToRight: Bool? if abs(velocity) > 10.0 { @@ -320,30 +336,53 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.panTransitionFraction = 0.0 self.searchContentNode.transitionFraction = nil - if let (layout, navigationHeight) = self.containerLayout { - let _ = self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) - } + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) default: break } } - func updateSearchQuery(_ query: String) { + private func updateSearchQuery(_ query: String) { + let queryUpdated = self.query != query self.query = query - let cleanQuery = cleanHashtag(query) - if !cleanQuery.isEmpty { - self.currentController?.beginMessageSearch("#" + cleanQuery) + if !query.isEmpty { + self.currentController?.beginMessageSearch(query) - self.myChatContents?.hashtagSearchUpdate(query: cleanQuery) - self.myController?.beginMessageSearch("#" + cleanQuery) + self.myChatContents?.hashtagSearchUpdate(query: query) + self.myController?.beginMessageSearch(query) - self.globalChatContents?.hashtagSearchUpdate(query: cleanQuery) - self.globalController?.beginMessageSearch("#" + cleanQuery) + self.globalChatContents?.hashtagSearchUpdate(query: query) + self.globalController?.beginMessageSearch(query) } - if let (layout, navigationHeight) = self.containerLayout { - let _ = self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) + if queryUpdated { + self.updateStorySearch() + } + + self.requestUpdate(transition: .immediate) + } + + private func updateStorySearch() { + self.globalStorySearchState = nil + self.globalStorySearchDisposable.set(nil) + self.globalStorySearchContext = nil + + if !self.query.isEmpty { + let globalStorySearchContext = SearchStoryListContext(account: self.context.account, source: .hashtag(self.query)) + self.globalStorySearchDisposable.set((globalStorySearchContext.state + |> deliverOnMainQueue).startStrict(next: { [weak self] state in + guard let self else { + return + } + if state.totalCount > 0 { + self.globalStorySearchState = state + } else { + self.globalStorySearchState = nil + } + self.requestUpdate(transition: .animated(duration: 0.25, curve: .easeInOut)) + })) + self.globalStorySearchContext = globalStorySearchContext } } @@ -351,9 +390,11 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.currentController?.cancelSelectingMessages() } - func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { - self.backgroundColor = theme.chatList.backgroundColor - self.searchContentNode.updateTheme(theme) + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + + self.backgroundColor = presentationData.theme.chatList.backgroundColor + self.searchContentNode.updateTheme(presentationData.theme) } func scrollToTop() { @@ -366,6 +407,12 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } } + func requestUpdate(transition: ContainedViewLayoutTransition) { + if let (layout, navigationHeight) = self.containerLayout { + let _ = self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: transition) + } + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { let isFirstTime = self.containerLayout == nil self.containerLayout = (layout, navigationBarHeight) @@ -407,8 +454,53 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } if let controller = self.globalController { + var topInset: CGFloat = insets.top - 89.0 + if let state = self.globalStorySearchState { + let componentView: ComponentView + var panelTransition = ComponentTransition(transition) + if let current = self.globalStorySearchComponentView { + componentView = current + } else { + panelTransition = .immediate + componentView = ComponentView() + self.globalStorySearchComponentView = componentView + } + let panelSize = componentView.update( + transition: .immediate, + component: AnyComponent(StoryResultsPanelComponent( + context: self.context, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + query: self.query, + state: state, + sideInset: layout.safeInsets.left, + action: { [weak self] in + guard let self else { + return + } + let searchController = self.context.sharedContext.makeStorySearchController(context: self.context, scope: .query(self.query), listContext: self.globalStorySearchContext) + self.controller?.push(searchController) + } + )), + environment: {}, + containerSize: layout.size + ) + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top - 36.0), size: panelSize) + if let view = componentView.view { + if view.superview == nil { + controller.view.addSubview(view) + view.layer.animatePosition(from: CGPoint(x: 0.0, y: -panelSize.height), to: .zero, duration: 0.25, additive: true) + } + panelTransition.setFrame(view: view, frame: panelFrame) + } + topInset += panelSize.height + } else if let globalStorySearchComponentView = self.globalStorySearchComponentView { + globalStorySearchComponentView.view?.removeFromSuperview() + self.globalStorySearchComponentView = nil + } + transition.updateFrame(node: controller.displayNode, frame: CGRect(origin: CGPoint(x: layout.size.width * 2.0, y: 0.0), size: layout.size)) - controller.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: insets.top - 89.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + controller.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) if controller.displayNode.supernode == nil { controller.viewWillAppear(false) @@ -458,14 +550,3 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } } } - -private func cleanHashtag(_ string: String) -> String { - var string = string - if string.hasPrefix("#") { - string.removeFirst() - } - if string.hasPrefix("$") { - string.removeFirst() - } - return string -} diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift index 4db226fcd84..3eabc0345c4 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift @@ -54,7 +54,7 @@ final class HashtagSearchGlobalChatContents: ChatCustomContentsProtocol { if self.publicPosts { search = self.context.engine.messages.searchHashtagPosts(hashtag: self.query, state: nil) } else { - search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil), query: "#\(self.query)", state: nil) + search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil), query: self.query, state: nil) } self.isSearchingPromise.set(true) @@ -102,7 +102,7 @@ final class HashtagSearchGlobalChatContents: ChatCustomContentsProtocol { if self.publicPosts { search = self.context.engine.messages.searchHashtagPosts(hashtag: self.query, state: self.currentSearchState) } else { - search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil), query: "#\(self.query)", state: currentSearchState) + search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil), query: self.query, state: currentSearchState) } self.historyViewDisposable?.dispose() diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift index ebd74ef10c9..33eb723ab76 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift @@ -66,8 +66,18 @@ final class HashtagSearchNavigationContentNode: NavigationBarContentNode { self.hasCurrentChat = hasCurrentChat self.cancel = cancel + + let icon: SearchBarNode.Icon + if initialQuery.hasPrefix("$") { + icon = .cashtag + } else { + icon = .hashtag + } + + var initialQuery = initialQuery + initialQuery.removeFirst() - self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern, icon: .hashtag, displayBackground: false) + self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern, icon: icon, displayBackground: false) self.searchBar.text = initialQuery self.searchBar.placeholderString = NSAttributedString(string: strings.HashtagSearch_SearchPlaceholder, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor) @@ -133,7 +143,7 @@ final class HashtagSearchNavigationContentNode: NavigationBarContentNode { items.append(TabSelectorComponent.Item(id: AnyHashable(2), title: self.strings.HashtagSearch_PublicPosts)) let tabSelectorSize = self.tabSelector.update( - transition: Transition(transition), + transition: ComponentTransition(transition), component: AnyComponent(TabSelectorComponent( colors: TabSelectorComponent.Colors( foreground: self.theme.list.itemSecondaryTextColor, diff --git a/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift b/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift new file mode 100644 index 00000000000..32a693623c6 --- /dev/null +++ b/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift @@ -0,0 +1,193 @@ +import Foundation +import Display +import ComponentFlow +import TelegramCore +import TelegramPresentationData +import MultilineTextComponent +import BundleIconComponent +import StorySetIndicatorComponent +import AccountContext + +final class StoryResultsPanelComponent: CombinedComponent { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let query: String + let state: StoryListContext.State + let sideInset: CGFloat + let action: () -> Void + + public init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + query: String, + state: StoryListContext.State, + sideInset: CGFloat, + action: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.query = query + self.state = state + self.sideInset = sideInset + self.action = action + } + + static func ==(lhs: StoryResultsPanelComponent, rhs: StoryResultsPanelComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.query != rhs.query { + return false + } + if lhs.state != rhs.state { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + return true + } + + static var body: Body { + let background = Child(Rectangle.self) + let avatars = Child(StorySetIndicatorComponent.self) + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + let arrow = Child(BundleIconComponent.self) + let separator = Child(Rectangle.self) + let button = Child(Button.self) + + return { context in + let component = context.component + + let spacing: CGFloat = 3.0 + + let textLeftInset: CGFloat = 81.0 + component.sideInset + let textTopInset: CGFloat = 9.0 + + var existingPeerIds = Set() + var items: [StorySetIndicatorComponent.Item] = [] + for item in component.state.items { + guard let peer = item.peer, !existingPeerIds.contains(peer.id) else { + continue + } + existingPeerIds.insert(peer.id) + items.append(StorySetIndicatorComponent.Item(storyItem: item.storyItem, peer: peer)) + } + + let avatars = avatars.update( + component: StorySetIndicatorComponent( + context: component.context, + strings: component.strings, + items: Array(items.prefix(3)), + displayAvatars: true, + hasUnseen: true, + hasUnseenPrivate: false, + totalCount: 0, + theme: component.theme, + action: {} + ), + availableSize: context.availableSize, + transition: .immediate + ) + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.strings.HashtagSearch_StoriesFound(Int32(component.state.totalCount)), + font: Font.semibold(15.0), + textColor: component.theme.rootController.navigationBar.primaryTextColor, + paragraphAlignment: .natural + )), + horizontalAlignment: .natural, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - textLeftInset, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let text = text.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.strings.HashtagSearch_StoriesFoundInfo(component.query).string, + font: Font.regular(14.0), + textColor: component.theme.rootController.navigationBar.secondaryTextColor, + paragraphAlignment: .natural + )), + horizontalAlignment: .natural, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - textLeftInset, height: context.availableSize.height), + transition: .immediate + ) + + let arrow = arrow.update( + component: BundleIconComponent( + name: "Item List/DisclosureArrow", + tintColor: component.theme.list.disclosureArrowColor + ), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), + transition: .immediate + ) + + let size = CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + spacing + text.size.height + textTopInset + 2.0) + + let background = background.update( + component: Rectangle(color: component.theme.rootController.navigationBar.opaqueBackgroundColor), + availableSize: size, + transition: .immediate + ) + + let separator = separator.update( + component: Rectangle(color: component.theme.rootController.navigationBar.separatorColor), + availableSize: CGSize(width: size.width, height: UIScreenPixel), + transition: .immediate + ) + + let button = button.update( + component: Button( + content: AnyComponent(Rectangle(color: .clear)), + action: component.action + ), + availableSize: size, + transition: .immediate + ) + + context.add(background + .position(CGPoint(x: background.size.width / 2.0, y: background.size.height / 2.0)) + ) + + context.add(separator + .position(CGPoint(x: background.size.width / 2.0, y: background.size.height - separator.size.height / 2.0)) + ) + + context.add(avatars + .position(CGPoint(x: component.sideInset + 10.0 + 30.0, y: background.size.height / 2.0)) + ) + + context.add(title + .position(CGPoint(x: textLeftInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + ) + + context.add(text + .position(CGPoint(x: textLeftInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) + ) + + context.add(arrow + .position(CGPoint(x: context.availableSize.width - arrow.size.width - component.sideInset, y: size.height / 2.0)) + ) + + context.add(button + .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + ) + + return size + } + } +} diff --git a/submodules/InstantPageUI/Sources/InstantPageDetailsNode.swift b/submodules/InstantPageUI/Sources/InstantPageDetailsNode.swift index ccb2c590816..38fff01023a 100644 --- a/submodules/InstantPageUI/Sources/InstantPageDetailsNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageDetailsNode.swift @@ -212,7 +212,7 @@ final class InstantPageDetailsArrowNode : ASDisplayNode { self.setNeedsDisplay() } } - private (set) var open: Bool + private(set) var open: Bool private var progress: CGFloat = 0.0 private var targetProgress: CGFloat? diff --git a/submodules/InstantPageUI/Sources/InstantPageLayout.swift b/submodules/InstantPageUI/Sources/InstantPageLayout.swift index a47eb382ab8..be4312b6e65 100644 --- a/submodules/InstantPageUI/Sources/InstantPageLayout.swift +++ b/submodules/InstantPageUI/Sources/InstantPageLayout.swift @@ -822,7 +822,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: } } - let map = TelegramMediaMap(latitude: latitude, longitude: longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + let map = TelegramMediaMap(latitude: latitude, longitude: longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) let attributes: [InstantPageImageAttribute] = [InstantPageMapAttribute(zoom: zoom, dimensions: dimensions.cgSize)] var contentSize = CGSize(width: boundingWidth - safeInset * 2.0, height: 0.0) diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index e46a510fd97..f522d8bb2d3 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -474,7 +474,7 @@ public class InvisibleInkDustNode: ASDisplayNode { } public func update(revealed: Bool, animated: Bool = true) { - guard self.isRevealed != revealed, let textNode = self.textNode else { + guard self.isRevealed != revealed else { return } @@ -483,11 +483,15 @@ public class InvisibleInkDustNode: ASDisplayNode { if revealed { let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .linear) : .immediate transition.updateAlpha(node: self, alpha: 0.0) - transition.updateAlpha(node: textNode, alpha: 1.0) + if let textNode = self.textNode { + transition.updateAlpha(node: textNode, alpha: 1.0) + } } else { let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .linear) : .immediate transition.updateAlpha(node: self, alpha: 1.0) - transition.updateAlpha(node: textNode, alpha: 0.0) + if let textNode = self.textNode { + transition.updateAlpha(node: textNode, alpha: 0.0) + } if self.isExploding { self.isExploding = false @@ -497,7 +501,7 @@ public class InvisibleInkDustNode: ASDisplayNode { } public func revealAtLocation(_ location: CGPoint) { - guard let (_, _, textColor, _, _) = self.currentParams, let textNode = self.textNode, !self.isRevealed else { + guard let (_, _, textColor, _, _) = self.currentParams, !self.isRevealed else { return } @@ -507,7 +511,7 @@ public class InvisibleInkDustNode: ASDisplayNode { self.isExploding = true self.emitterLayer?.setValue(true, forKeyPath: "emitterBehaviors.fingerAttractor.enabled") - self.emitterLayer?.setValue(position, forKeyPath: "emitterBehaviors.fingerAttractor.position") + self.emitterLayer?.setValue(location, forKeyPath: "emitterBehaviors.fingerAttractor.position") let maskSize = self.emitterNode.frame.size Queue.concurrentDefaultQueue().async { @@ -520,10 +524,15 @@ public class InvisibleInkDustNode: ASDisplayNode { } } - Queue.mainQueue().after(0.1 * UIView.animationDurationFactor()) { - textNode.alpha = 1.0 + Queue.mainQueue().after(0.1 * UIView.animationDurationFactor()) { [weak self] in + guard let self else { + return + } - textNode.view.mask = self.textMaskNode.view + if let textNode = self.textNode { + textNode.alpha = 1.0 + textNode.view.mask = self.textMaskNode.view + } self.textSpotNode.frame = CGRect(x: 0.0, y: 0.0, width: self.emitterMaskNode.frame.width * 3.0, height: self.emitterMaskNode.frame.height * 3.0) let xFactor = (location.x / self.emitterNode.frame.width - 0.5) * 2.0 @@ -539,8 +548,13 @@ public class InvisibleInkDustNode: ASDisplayNode { self.textSpotNode.layer.anchorPoint = CGPoint(x: location.x / self.emitterMaskNode.frame.width, y: location.y / self.emitterMaskNode.frame.height) self.textSpotNode.position = location - self.textSpotNode.layer.animateScale(from: 0.3333, to: 10.5 + scaleAddition, duration: 0.55 + durationAddition, removeOnCompletion: false, completion: { _ in - textNode.view.mask = nil + self.textSpotNode.layer.animateScale(from: 0.3333, to: 10.5 + scaleAddition, duration: 0.55 + durationAddition, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + if let textNode = self.textNode { + textNode.view.mask = nil + } }) self.textSpotNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) @@ -567,14 +581,39 @@ public class InvisibleInkDustNode: ASDisplayNode { self.emitterMaskFillNode.layer.removeAllAnimations() } } else { - textNode.alpha = 1.0 - textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + if let textNode = self.textNode { + textNode.alpha = 1.0 + textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } self.staticNode?.alpha = 0.0 self.staticNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) } } + public func revealWithoutMaskAtLocation(_ location: CGPoint) { + guard !self.isRevealed else { + return + } + + self.isRevealed = true + + if self.enableAnimations { + self.isExploding = true + + self.emitterLayer?.setValue(true, forKeyPath: "emitterBehaviors.fingerAttractor.enabled") + self.emitterLayer?.setValue(location, forKeyPath: "emitterBehaviors.fingerAttractor.position") + + Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) { + self.isExploding = false + self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled") + } + } else { + self.staticNode?.alpha = 0.0 + self.staticNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + } + } + @objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) { let location = gestureRecognizer.location(in: self.view) self.revealAtLocation(location) diff --git a/submodules/ItemListUI/Sources/ItemListControllerSegmentedTitleView.swift b/submodules/ItemListUI/Sources/ItemListControllerSegmentedTitleView.swift index 4af1edb6f2d..a74a317d8e6 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerSegmentedTitleView.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerSegmentedTitleView.swift @@ -57,7 +57,7 @@ public final class ItemListControllerSegmentedTitleView: UIView { self.update(transition: .immediate) } - private func update(transition: Transition) { + private func update(transition: ComponentTransition) { guard let size = self.validLayout else { return } diff --git a/submodules/ItemListUI/Sources/ItemListControllerTabsContentNode.swift b/submodules/ItemListUI/Sources/ItemListControllerTabsContentNode.swift index e409a0e4d4a..c629bd0b4e0 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerTabsContentNode.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerTabsContentNode.swift @@ -78,7 +78,7 @@ final class ItemListControllerTabsContentNode: NavigationBarContentNode { } let tabSelectorSize = self.tabSelector.update( - transition: Transition(transition), + transition: ComponentTransition(transition), component: AnyComponent(TabSelectorComponent( colors: TabSelectorComponent.Colors( foreground: self.theme.list.itemSecondaryTextColor, diff --git a/submodules/JoinLinkPreviewUI/BUILD b/submodules/JoinLinkPreviewUI/BUILD index 1234038005c..f7ff8671e67 100644 --- a/submodules/JoinLinkPreviewUI/BUILD +++ b/submodules/JoinLinkPreviewUI/BUILD @@ -20,11 +20,11 @@ swift_library( "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/ShareController:ShareController", "//submodules/SelectablePeerNode:SelectablePeerNode", - "//submodules/PeerInfoUI:PeerInfoUI", "//submodules/UndoUI:UndoUI", "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/ComponentFlow", "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", ], visibility = [ "//visibility:public", diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift index 5bbf773208b..fde42317a17 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift @@ -8,8 +8,8 @@ import TelegramPresentationData import AccountContext import AlertUI import PresentationDataUtils -import PeerInfoUI import UndoUI +import OldChannelsController public final class JoinLinkPreviewController: ViewController { private var controllerNode: JoinLinkPreviewControllerNode { diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift index 756cd33f567..60b8ea8e4c6 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift @@ -297,7 +297,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer let animationRenderer = self.context.animationRenderer let avatarIcon: ComponentView - var avatarIconTransition = Transition(transition) + var avatarIconTransition = ComponentTransition(transition) if let current = self.avatarIcon { avatarIcon = current } else { diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCamera.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCamera.h index 9fa2410cae6..b20f65fd31d 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCamera.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCamera.h @@ -85,6 +85,8 @@ typedef enum @property (nonatomic, assign) CGFloat zoomLevel; @property (nonatomic, readonly) CGFloat minZoomLevel; @property (nonatomic, readonly) CGFloat maxZoomLevel; +@property (nonatomic, readonly) int32_t maxMarkZoomValue; +@property (nonatomic, readonly) int32_t secondMarkZoomValue; - (void)setZoomLevel:(CGFloat)zoomLevel animated:(bool)animated; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h index 2a816abd14f..04ef8a5a7a5 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h @@ -30,6 +30,9 @@ @property (nonatomic, readonly) CGFloat minZoomLevel; @property (nonatomic, readonly) CGFloat maxZoomLevel; +@property (nonatomic, readonly) int32_t maxMarkZoomValue; +@property (nonatomic, readonly) int32_t secondMarkZoomValue; + - (void)setZoomLevel:(CGFloat)zoomLevel animated:(bool)animated; @property (nonatomic, readonly) bool hasUltrawideCamera; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraVolumeButtonHandler.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraVolumeButtonHandler.h index a958f1a9f42..73c52072fac 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraVolumeButtonHandler.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraVolumeButtonHandler.h @@ -1,11 +1,12 @@ #import +#import @interface PGCameraVolumeButtonHandler : NSObject @property (nonatomic, assign) bool enabled; @property (nonatomic, assign) bool ignoring; -- (instancetype)initWithUpButtonPressedBlock:(void (^)(void))upButtonPressedBlock upButtonReleasedBlock:(void (^)(void))upButtonReleasedBlock downButtonPressedBlock:(void (^)(void))downButtonPressedBlock downButtonReleasedBlock:(void (^)(void))downButtonReleasedBlock; +- (instancetype)initWithIsCameraSpecific:(bool)isCameraSpecific eventView:(UIView *)eventView upButtonPressedBlock:(void (^)(void))upButtonPressedBlock upButtonReleasedBlock:(void (^)(void))upButtonReleasedBlock downButtonPressedBlock:(void (^)(void))downButtonPressedBlock downButtonReleasedBlock:(void (^)(void))downButtonReleasedBlock; - (void)enableIn:(NSTimeInterval)timeInterval; - (void)disableFor:(NSTimeInterval)timeInterval; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraMainView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraMainView.h index 540134b897e..3854bd1dfa6 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraMainView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraMainView.h @@ -17,6 +17,7 @@ @class TGMediaPickerPhotoStripView; @class TGMediaPickerGallerySelectedItemsModel; @class TGMediaEditingContext; +@class PGCamera; @interface TGCameraCornersView : UIImageView @@ -67,7 +68,7 @@ @property (nonatomic, assign) CGRect previewViewFrame; -- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera; +- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera camera:(PGCamera *)camera; - (void)setDocumentFrameHidden:(bool)hidden; - (void)setCameraMode:(PGCameraMode)mode; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h index 7871f47ecc3..01b5704dddd 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h @@ -92,6 +92,12 @@ - (void)setSpoiler:(bool)spoiler forItem:(NSObject *)item; - (SSignal *)spoilersUpdatedSignal; +- (NSNumber *)priceForItem:(NSObject *)item; +- (SSignal *)priceSignalForItem:(NSObject *)item; +- (SSignal *)priceSignalForIdentifier:(NSString *)identifier; +- (void)setPrice:(NSNumber *)price forItem:(NSObject *)item; +- (SSignal *)pricesUpdatedSignal; + - (UIImage *)paintingImageForItem:(NSObject *)item; - (UIImage *)stillPaintingImageForItem:(NSObject *)item; - (bool)setPaintingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage forItem:(NSObject *)item dataUrl:(NSURL **)dataOutUrl entitiesDataUrl:(NSURL **)entitiesDataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaSelectionContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaSelectionContext.h index bc3c16874cd..68bda51e980 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaSelectionContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaSelectionContext.h @@ -11,7 +11,7 @@ - (instancetype)initWithGroupingAllowed:(bool)allowGrouping selectionLimit:(int)selectionLimit; @property (nonatomic, readonly) bool allowGrouping; -@property (nonatomic, readonly) int selectionLimit; +@property (nonatomic, assign) int selectionLimit; @property (nonatomic, copy) void (^selectionLimitExceeded)(void); @property (nonatomic, copy) bool (^attemptSelectingItem)(id); diff --git a/submodules/LegacyComponents/Sources/PGCamera.m b/submodules/LegacyComponents/Sources/PGCamera.m index 2e4348a9eb1..c06c9db451d 100644 --- a/submodules/LegacyComponents/Sources/PGCamera.m +++ b/submodules/LegacyComponents/Sources/PGCamera.m @@ -765,6 +765,14 @@ - (void)setZoomLevel:(CGFloat)zoomLevel animated:(bool)animated }]; } +- (int32_t)maxMarkZoomValue { + return self.captureSession.maxMarkZoomValue; +} + +- (int32_t)secondMarkZoomValue { + return self.captureSession.secondMarkZoomValue; +} + #pragma mark - Device Angle - (void)startDeviceAngleMeasuring diff --git a/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m b/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m index 28b7f4a23cc..fe40edfcbac 100644 --- a/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m +++ b/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m @@ -551,6 +551,14 @@ - (void)setZoomLevel:(CGFloat)zoomLevel { [self setZoomLevel:zoomLevel animated:false]; } +- (int32_t)maxMarkZoomValue { + return 25.0; +} + +- (int32_t)secondMarkZoomValue { + return 5.0; +} + - (void)setZoomLevel:(CGFloat)zoomLevel animated:(bool)animated { if (![self.videoDevice respondsToSelector:@selector(setVideoZoomFactor:)]) diff --git a/submodules/LegacyComponents/Sources/PGCameraVolumeButtonHandler.m b/submodules/LegacyComponents/Sources/PGCameraVolumeButtonHandler.m index 1b4d63d5b03..7cd069cbc1c 100644 --- a/submodules/LegacyComponents/Sources/PGCameraVolumeButtonHandler.m +++ b/submodules/LegacyComponents/Sources/PGCameraVolumeButtonHandler.m @@ -5,6 +5,8 @@ #import "TGStringUtils.h" #import "Freedom.h" +#import + static NSString *encodeText(NSString *string, int key) { NSMutableString *result = [[NSMutableString alloc] init]; @@ -19,8 +21,11 @@ @interface PGCameraVolumeButtonHandler () { id _dataSource; + id _eventInteraction; } +@property (nonatomic, weak) UIView *eventView; + @property (nonatomic, copy) void(^upButtonPressedBlock)(void); @property (nonatomic, copy) void(^upButtonReleasedBlock)(void); @property (nonatomic, copy) void(^downButtonPressedBlock)(void); @@ -30,11 +35,13 @@ @interface PGCameraVolumeButtonHandler () { @implementation PGCameraVolumeButtonHandler -- (instancetype)initWithUpButtonPressedBlock:(void (^)(void))upButtonPressedBlock upButtonReleasedBlock:(void (^)(void))upButtonReleasedBlock downButtonPressedBlock:(void (^)(void))downButtonPressedBlock downButtonReleasedBlock:(void (^)(void))downButtonReleasedBlock +- (instancetype)initWithIsCameraSpecific:(bool)isCameraSpecific eventView:(UIView *)eventView upButtonPressedBlock:(void (^)(void))upButtonPressedBlock upButtonReleasedBlock:(void (^)(void))upButtonReleasedBlock downButtonPressedBlock:(void (^)(void))downButtonPressedBlock downButtonReleasedBlock:(void (^)(void))downButtonReleasedBlock { self = [super init]; if (self != nil) { + self.eventView = eventView; + self.upButtonPressedBlock = upButtonPressedBlock; self.upButtonReleasedBlock = upButtonReleasedBlock; self.downButtonPressedBlock = downButtonPressedBlock; @@ -45,9 +52,47 @@ - (instancetype)initWithUpButtonPressedBlock:(void (^)(void))upButtonPressedBloc self.enabled = true; if (@available(iOS 17.2, *)) { - NSString *className = encodeText(@"NQWpmvnfDpouspmmfsTztufnEbubTpvsdf", -1); - Class c = NSClassFromString(className); - _dataSource = [[c alloc] init]; + if (isCameraSpecific) { + __weak PGCameraVolumeButtonHandler *weakSelf = self; + AVCaptureEventInteraction *interaction = [[AVCaptureEventInteraction alloc] initWithPrimaryEventHandler:^(AVCaptureEvent * _Nonnull event) { + __strong PGCameraVolumeButtonHandler *strongSelf = weakSelf; + switch (event.phase) { + case AVCaptureEventPhaseBegan: + strongSelf.downButtonPressedBlock(); + break; + case AVCaptureEventPhaseEnded: + strongSelf.downButtonReleasedBlock(); + break; + case AVCaptureEventPhaseCancelled: + strongSelf.downButtonReleasedBlock(); + break; + default: + break; + } + } secondaryEventHandler:^(AVCaptureEvent * _Nonnull event) { + __strong PGCameraVolumeButtonHandler *strongSelf = weakSelf; + switch (event.phase) { + case AVCaptureEventPhaseBegan: + strongSelf.upButtonPressedBlock(); + break; + case AVCaptureEventPhaseEnded: + strongSelf.upButtonReleasedBlock(); + break; + case AVCaptureEventPhaseCancelled: + strongSelf.upButtonReleasedBlock(); + break; + default: + break; + } + }]; + interaction.enabled = true; + [eventView addInteraction:interaction]; + _eventInteraction = interaction; + } else { + NSString *className = encodeText(@"NQWpmvnfDpouspmmfsTztufnEbubTpvsdf", -1); + Class c = NSClassFromString(className); + _dataSource = [[c alloc] init]; + } } } return self; @@ -55,6 +100,10 @@ - (instancetype)initWithUpButtonPressedBlock:(void (^)(void))upButtonPressedBloc - (void)dealloc { + if (_eventInteraction != nil) { + [self.eventView removeInteraction:_eventInteraction]; + } + self.enabled = false; [[NSNotificationCenter defaultCenter] removeObserver:self]; } diff --git a/submodules/LegacyComponents/Sources/TGCameraController.m b/submodules/LegacyComponents/Sources/TGCameraController.m index 985cfbb9455..e1b3d911342 100644 --- a/submodules/LegacyComponents/Sources/TGCameraController.m +++ b/submodules/LegacyComponents/Sources/TGCameraController.m @@ -307,12 +307,12 @@ - (void)loadView if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { - _interfaceView = [[TGCameraMainPhoneView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent videoModeByDefault:_intent == TGCameraControllerGenericVideoOnlyIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera]; + _interfaceView = [[TGCameraMainPhoneView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent videoModeByDefault:_intent == TGCameraControllerGenericVideoOnlyIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera camera:_camera]; [_interfaceView setInterfaceOrientation:interfaceOrientation animated:false]; } else { - _interfaceView = [[TGCameraMainTabletView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent videoModeByDefault:_intent == TGCameraControllerGenericVideoOnlyIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera]; + _interfaceView = [[TGCameraMainTabletView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent videoModeByDefault:_intent == TGCameraControllerGenericVideoOnlyIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera camera:_camera]; [_interfaceView setInterfaceOrientation:interfaceOrientation animated:false]; CGSize referenceSize = [self referenceViewSizeForOrientation:interfaceOrientation]; @@ -510,7 +510,7 @@ - (void)loadView strongSelf->_interfaceView.shutterReleased(true); }; - _buttonHandler = [[PGCameraVolumeButtonHandler alloc] initWithUpButtonPressedBlock:buttonPressed upButtonReleasedBlock:buttonReleased downButtonPressedBlock:buttonPressed downButtonReleasedBlock:buttonReleased]; + _buttonHandler = [[PGCameraVolumeButtonHandler alloc] initWithIsCameraSpecific:true eventView:self.view upButtonPressedBlock:buttonPressed upButtonReleasedBlock:buttonReleased downButtonPressedBlock:buttonPressed downButtonReleasedBlock:buttonReleased]; [self _configureCamera]; } @@ -806,29 +806,29 @@ - (void)_configureCamera } }; - _camera.captureSession.crossfadeNeeded = ^{ - __strong TGCameraController *strongSelf = weakSelf; - if (strongSelf != nil) - { - if (strongSelf->_crossfadingForZoom) { - return; - } - strongSelf->_crossfadingForZoom = true; - - [strongSelf->_camera captureNextFrameCompletion:^(UIImage *image) - { - TGDispatchOnMainThread(^ - { - [strongSelf->_previewView beginTransitionWithSnapshotImage:image animated:false]; - - TGDispatchAfter(0.15, dispatch_get_main_queue(), ^{ - [strongSelf->_previewView endTransitionAnimated:true]; - strongSelf->_crossfadingForZoom = false; - }); - }); - }]; - }; - }; +// _camera.captureSession.crossfadeNeeded = ^{ +// __strong TGCameraController *strongSelf = weakSelf; +// if (strongSelf != nil) +// { +// if (strongSelf->_crossfadingForZoom) { +// return; +// } +// strongSelf->_crossfadingForZoom = true; +// +// [strongSelf->_camera captureNextFrameCompletion:^(UIImage *image) +// { +// TGDispatchOnMainThread(^ +// { +// [strongSelf->_previewView beginTransitionWithSnapshotImage:image animated:false]; +// +// TGDispatchAfter(0.15, dispatch_get_main_queue(), ^{ +// [strongSelf->_previewView endTransitionAnimated:true]; +// strongSelf->_crossfadingForZoom = false; +// }); +// }); +// }]; +// }; +// }; } #pragma mark - View Life Cycle diff --git a/submodules/LegacyComponents/Sources/TGCameraMainPhoneView.m b/submodules/LegacyComponents/Sources/TGCameraMainPhoneView.m index a8741b4b1d0..fcca1b123f8 100644 --- a/submodules/LegacyComponents/Sources/TGCameraMainPhoneView.m +++ b/submodules/LegacyComponents/Sources/TGCameraMainPhoneView.m @@ -101,7 +101,7 @@ @implementation TGCameraMainPhoneView @synthesize cancelPressed; @synthesize actionHandle = _actionHandle; -- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera +- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera camera:(PGCamera *)camera { self = [super initWithFrame:frame]; if (self != nil) diff --git a/submodules/LegacyComponents/Sources/TGCameraMainTabletView.m b/submodules/LegacyComponents/Sources/TGCameraMainTabletView.m index 632527b333a..c4ce7240459 100644 --- a/submodules/LegacyComponents/Sources/TGCameraMainTabletView.m +++ b/submodules/LegacyComponents/Sources/TGCameraMainTabletView.m @@ -42,7 +42,7 @@ @implementation TGCameraMainTabletView @synthesize shutterReleased; @synthesize cancelPressed; -- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera +- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera camera:(PGCamera *)camera { self = [super initWithFrame:frame]; if (self != nil) diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m index 8c09b639d01..94f4f266326 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m @@ -942,6 +942,7 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti NSInteger num = 0; bool grouping = selectionContext.grouping; + NSNumber *price; bool hasAnyTimers = false; if (editingContext != nil || grouping) { @@ -950,6 +951,9 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti if ([editingContext timerForItem:asset] != nil) { hasAnyTimers = true; } + if (price == nil) { + price = [editingContext priceForItem:asset]; + } id adjustments = [editingContext adjustmentsForItem:asset]; if ([adjustments isKindOfClass:[TGVideoEditAdjustments class]]) { TGVideoEditAdjustments *videoAdjustments = (TGVideoEditAdjustments *)adjustments; @@ -1057,6 +1061,9 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti else if (groupedId != nil && !hasAnyTimers) dict[@"groupedId"] = groupedId; + if (price != nil) + dict[@"price"] = price; + if (spoiler) { dict[@"spoiler"] = @true; } @@ -1137,6 +1144,9 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti else if (groupedId != nil && !hasAnyTimers) dict[@"groupedId"] = groupedId; + if (price != nil) + dict[@"price"] = price; + if (spoiler) { dict[@"spoiler"] = @true; } @@ -1217,6 +1227,9 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti else if (groupedId != nil && !hasAnyTimers) dict[@"groupedId"] = groupedId; + if (price != nil) + dict[@"price"] = price; + if (spoiler) { dict[@"spoiler"] = @true; } @@ -1261,6 +1274,9 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti if (groupedId != nil) dict[@"groupedId"] = groupedId; + if (price != nil) + dict[@"price"] = price; + if (spoiler) { dict[@"spoiler"] = @true; } @@ -1334,6 +1350,9 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti else if (groupedId != nil && !hasAnyTimers) dict[@"groupedId"] = groupedId; + if (price != nil) + dict[@"price"] = price; + if (spoiler) { dict[@"spoiler"] = @true; } @@ -1415,6 +1434,9 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti if (timer != nil) dict[@"timer"] = timer; + if (price != nil) + dict[@"price"] = price; + if (spoiler) { dict[@"spoiler"] = @true; } diff --git a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m index 5578835d546..a9aa9b8e2e6 100644 --- a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m +++ b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m @@ -64,6 +64,16 @@ + (instancetype)spoilerUpdate:(bool)spoiler; @end +@interface TGMediaPriceUpdate : NSObject + +@property (nonatomic, readonly, strong) id item; +@property (nonatomic, readonly, strong) NSNumber *price; + ++ (instancetype)priceUpdateWithItem:(id)item price:(NSNumber *)price; ++ (instancetype)priceUpdate:(NSNumber *)timer; + +@end + @interface TGModernCache (Private) @@ -81,7 +91,8 @@ @interface TGMediaEditingContext () NSNumber *_timer; NSMutableDictionary *_spoilers; - + NSMutableDictionary *_prices; + SQueue *_queue; NSMutableDictionary *_temporaryRepCache; @@ -112,6 +123,7 @@ @interface TGMediaEditingContext () SPipe *_captionPipe; SPipe *_timerPipe; SPipe *_spoilerPipe; + SPipe *_pricePipe; SPipe *_fullSizePipe; SPipe *_cropPipe; @@ -133,6 +145,7 @@ - (instancetype)init _adjustments = [[NSMutableDictionary alloc] init]; _timers = [[NSMutableDictionary alloc] init]; _spoilers = [[NSMutableDictionary alloc] init]; + _prices = [[NSMutableDictionary alloc] init]; _imageCache = [[TGMemoryImageCache alloc] initWithSoftMemoryLimit:[[self class] imageSoftMemoryLimit] hardMemoryLimit:[[self class] imageHardMemoryLimit]]; @@ -180,6 +193,7 @@ - (instancetype)init _captionPipe = [[SPipe alloc] init]; _timerPipe = [[SPipe alloc] init]; _spoilerPipe = [[SPipe alloc] init]; + _pricePipe = [[SPipe alloc] init]; _fullSizePipe = [[SPipe alloc] init]; _cropPipe = [[SPipe alloc] init]; } @@ -676,6 +690,74 @@ - (SSignal *)spoilersUpdatedSignal }]; } +#pragma mark - + +- (NSNumber *)priceForItem:(NSObject *)item +{ + NSString *itemId = [self _contextualIdForItemId:item.uniqueIdentifier]; + if (itemId == nil) + return nil; + + return [self _priceForItemId:itemId]; +} + +- (NSNumber *)_priceForItemId:(NSString *)itemId +{ + if (itemId == nil) + return nil; + + return _prices[itemId]; +} + +- (void)setPrice:(NSNumber *)price forItem:(NSObject *)item +{ + NSString *itemId = [self _contextualIdForItemId:item.uniqueIdentifier]; + if (itemId == nil) + return; + + if (price.integerValue != 0) + _prices[itemId] = price; + else + [_prices removeObjectForKey:itemId]; + + _pricePipe.sink([TGMediaPriceUpdate priceUpdateWithItem:item price:price]); +} + +- (SSignal *)priceSignalForItem:(NSObject *)item +{ + SSignal *updateSignal = [[_pricePipe.signalProducer() filter:^bool(TGMediaPriceUpdate *update) + { + return [update.item.uniqueIdentifier isEqualToString:item.uniqueIdentifier]; + }] map:^NSNumber *(TGMediaPriceUpdate *update) + { + return update.price; + }]; + + return [[SSignal single:[self priceForItem:item]] then:updateSignal]; +} + +- (SSignal *)priceSignalForIdentifier:(NSString *)identifier +{ + SSignal *updateSignal = [[_pricePipe.signalProducer() filter:^bool(TGMediaPriceUpdate *update) + { + return [update.item.uniqueIdentifier isEqualToString:identifier]; + }] map:^NSNumber *(TGMediaPriceUpdate *update) + { + return update.price; + }]; + + return [[SSignal single:[self _priceForItemId:identifier]] then:updateSignal]; +} + +- (SSignal *)pricesUpdatedSignal +{ + return [_pricePipe.signalProducer() map:^id(__unused id value) + { + return @true; + }]; +} + + #pragma mark - - (void)setImage:(UIImage *)image thumbnailImage:(UIImage *)thumbnailImage forItem:(id)item synchronous:(bool)synchronous @@ -1189,3 +1271,23 @@ + (instancetype)spoilerUpdate:(bool)spoiler } @end + + +@implementation TGMediaPriceUpdate + ++ (instancetype)priceUpdateWithItem:(id)item price:(NSNumber *)price +{ + TGMediaPriceUpdate *update = [[TGMediaPriceUpdate alloc] init]; + update->_item = item; + update->_price = price; + return update; +} + ++ (instancetype)priceUpdate:(NSNumber *)price +{ + TGMediaPriceUpdate *update = [[TGMediaPriceUpdate alloc] init]; + update->_price = price; + return update; +} + +@end diff --git a/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m b/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m index 8c40c0d823a..d7da6d04b23 100644 --- a/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m +++ b/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m @@ -448,7 +448,7 @@ - (void)loadView [self.view addGestureRecognizer:_pinchGestureRecognizer]; void (^voidBlock)(void) = ^{}; - _buttonHandler = [[PGCameraVolumeButtonHandler alloc] initWithUpButtonPressedBlock:voidBlock upButtonReleasedBlock:voidBlock downButtonPressedBlock:voidBlock downButtonReleasedBlock:voidBlock]; + _buttonHandler = [[PGCameraVolumeButtonHandler alloc] initWithIsCameraSpecific:true eventView:self.view upButtonPressedBlock:voidBlock upButtonReleasedBlock:voidBlock downButtonPressedBlock:voidBlock downButtonReleasedBlock:voidBlock]; [self configureCamera]; } diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index eabf62b742e..022e49295ae 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -137,13 +137,15 @@ private final class LegacyAssetItemWrapper: NSObject { let item: LegacyAssetItem let timer: Int? let spoiler: Bool? + let price: Int64? let groupedId: Int64? let uniqueId: String? - init(item: LegacyAssetItem, timer: Int?, spoiler: Bool?, groupedId: Int64?, uniqueId: String?) { + init(item: LegacyAssetItem, timer: Int?, spoiler: Bool?, price: Int64?, groupedId: Int64?, uniqueId: String?) { self.item = item self.timer = timer self.spoiler = spoiler + self.price = price self.groupedId = groupedId self.uniqueId = uniqueId @@ -162,6 +164,9 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str return nil } } ?? [] + + let price = dict["price"] as? Int64 + if (dict["type"] as! NSString) == "editedPhoto" || (dict["type"] as! NSString) == "capturedPhoto" { let image = dict["image"] as! UIImage let thumbnail = dict["previewImage"] as? UIImage @@ -171,10 +176,10 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str let url: String? = (dict["url"] as? String) ?? (dict["url"] as? URL)?.path if let url = url { let dimensions = image.size - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: 4.0), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: false, asAnimation: true, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: 4.0), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: false, asAnimation: true, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) } } else { - result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .image(image), thumbnail: thumbnail, caption: caption, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .image(image), thumbnail: thumbnail, caption: caption, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) } return result } else if (dict["type"] as! NSString) == "cloudPhoto" { @@ -195,9 +200,9 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str name = customName } - result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .asset(asset.backingAsset), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: nil, spoiler: nil, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .asset(asset.backingAsset), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: nil, spoiler: nil, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) } else { - result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .asset(asset.backingAsset), thumbnail: thumbnail, caption: caption, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .asset(asset.backingAsset), thumbnail: thumbnail, caption: caption, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) } return result } else if (dict["type"] as! NSString) == "file" { @@ -218,12 +223,12 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str let dimensions = (dict["dimensions"]! as AnyObject).cgSizeValue! let duration = (dict["duration"]! as AnyObject).doubleValue! - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: tempFileUrl.path, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: nil, caption: caption, asFile: false, asAnimation: true, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: tempFileUrl.path, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: nil, caption: caption, asFile: false, asAnimation: true, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) return result } var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .tempFile(tempFileUrl.path), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .tempFile(tempFileUrl.path), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) return result } } else if (dict["type"] as! NSString) == "video" { @@ -235,13 +240,13 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str if let asset = dict["asset"] as? TGMediaAsset { var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) return result } else if let url = (dict["url"] as? String) ?? (dict["url"] as? URL)?.absoluteString { let dimensions = (dict["dimensions"]! as AnyObject).cgSizeValue! let duration = (dict["duration"]! as AnyObject).doubleValue! var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) return result } } else if (dict["type"] as! NSString) == "cameraVideo" { @@ -257,7 +262,7 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str let dimensions = previewImage.pixelSize() let duration = (dict["duration"]! as AnyObject).doubleValue! var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) return result } } @@ -433,6 +438,7 @@ private func ngMapLegacyAssetPickerValues( item: .video(data: data, thumbnail: thumbnail, adjustments: chunkAdjustments, caption: chunkCaption, asFile: asFile, asAnimation: asAnimation, stickers: stickers), timer: itemWrapper.timer, spoiler: itemWrapper.spoiler, + price: itemWrapper.price, groupedId: nil, uniqueId: chunkUniqueId ) @@ -458,6 +464,15 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A return Signal { subscriber in let disposable = SSignal.combineSignals(signals).start(next: { anyValues in var messages: [LegacyAssetPickerEnqueueMessage] = [] + + struct EnqueuePaidMessage { + var price: Int64 + var text: String + var entities: [MessageTextEntity] + var media: [Media] + } + + var paidMessage: EnqueuePaidMessage? // MARK: Nicegram RoundedVideos let anyValues = ngMapLegacyAssetPickerValues( @@ -545,7 +560,26 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A } } } - messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: false)) + + if let price = item.price { + if var current = paidMessage { + if current.text.isEmpty && !text.string.isEmpty { + current.text = text.string + current.entities = entities + } + current.media.append(media) + paidMessage = current + } else { + paidMessage = EnqueuePaidMessage( + price: price, + text: text.string, + entities: entities, + media: [media] + ) + } + } else { + messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: false)) + } } } case let .asset(asset): @@ -619,7 +653,25 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A } } - messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: false)) + if let price = item.price { + if var current = paidMessage { + if current.text.isEmpty && !text.string.isEmpty { + current.text = text.string + current.entities = entities + } + current.media.append(media) + paidMessage = current + } else { + paidMessage = EnqueuePaidMessage( + price: price, + text: text.string, + entities: entities, + media: [media] + ) + } + } else { + messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: false)) + } } } } @@ -669,7 +721,25 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A } } - messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: false)) + if let price = item.price { + if var current = paidMessage { + if current.text.isEmpty && !text.string.isEmpty { + current.text = text.string + current.entities = entities + } + current.media.append(media) + paidMessage = current + } else { + paidMessage = EnqueuePaidMessage( + price: price, + text: text.string, + entities: entities, + media: [media] + ) + } + } else { + messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: false)) + } } case .tempFile: break @@ -721,7 +791,25 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A } } - messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: true)) + if let price = item.price { + if var current = paidMessage { + if current.text.isEmpty && !text.string.isEmpty { + current.text = text.string + current.entities = entities + } + current.media.append(media) + paidMessage = current + } else { + paidMessage = EnqueuePaidMessage( + price: price, + text: text.string, + entities: entities, + media: [media] + ) + } + } else { + messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: true)) + } case let .asset(asset): var randomId: Int64 = 0 arc4random_buf(&randomId, 8) @@ -756,7 +844,25 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A } } - messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: true)) + if let price = item.price { + if var current = paidMessage { + if current.text.isEmpty && !text.string.isEmpty { + current.text = text.string + current.entities = entities + } + current.media.append(media) + paidMessage = current + } else { + paidMessage = EnqueuePaidMessage( + price: price, + text: text.string, + entities: entities, + media: [media] + ) + } + } else { + messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: true)) + } default: break } @@ -959,26 +1065,73 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A } // - // MARK: Nicegram RoundedVideos, change (text, localGroupingKey) - messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: messageText, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: localGroupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: asFile)) - - // MARK: Nicegram RoundedVideos - if sendAsRoundedVideo { - let videoMessageText = text - .string - .trimmingCharacters(in: .whitespacesAndNewlines) + if let price = item.price { + if var current = paidMessage { + if current.text.isEmpty && !text.string.isEmpty { + current.text = text.string + current.entities = entities + } + current.media.append(media) + paidMessage = current + } else { + paidMessage = EnqueuePaidMessage( + price: price, + text: text.string, + entities: entities, + media: [media] + ) + } + } else { + // MARK: Nicegram RoundedVideos, change (text, localGroupingKey) + messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: messageText, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: localGroupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: asFile)) - if !videoMessageText.isEmpty { - var uniqueId = item.uniqueId - uniqueId?.append("-text") - messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: videoMessageText, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: localGroupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []), uniqueId: uniqueId, isFile: false)) + // MARK: Nicegram RoundedVideos + if sendAsRoundedVideo { + let videoMessageText = text + .string + .trimmingCharacters(in: .whitespacesAndNewlines) + + if !videoMessageText.isEmpty { + var uniqueId = item.uniqueId + uniqueId?.append("-text") + messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: videoMessageText, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: localGroupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []), uniqueId: uniqueId, isFile: false)) + } } + // } - // } } } + if let paidMessage { + var attributes: [MessageAttribute] = [] + if !paidMessage.entities.isEmpty { + attributes.append(TextEntitiesMessageAttribute(entities: paidMessage.entities)) + } + messages.append( + LegacyAssetPickerEnqueueMessage( + message: .message( + text: paidMessage.text, + attributes: attributes, + inlineStickers: [:], + mediaReference: .standalone( + media: TelegramMediaPaidContent( + amount: paidMessage.price, + extendedMedia: paidMessage.media.map { .full(media: $0) } + )), + threadId: nil, + replyToMessageId: nil, + replyToStoryId: nil, + localGroupingKey: nil, + correlationId: nil, + bubbleUpEmojiOrStickersets: [] + ), + uniqueId: nil, + isFile: false + ) + ) + } + subscriber.putNext(messages) subscriber.putCompletion() }, error: { _ in diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index 145a4089823..d6dbd050291 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -147,13 +147,7 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity { case let .image(image, _): self.file = nil self.imagePromise.set(.single(image)) - case .animatedImage: - self.file = nil - case .video: - self.file = nil - case .dualVideoReference: - self.file = nil - case .message: + case .animatedImage, .video, .dualVideoReference, .message: self.file = nil } } @@ -616,8 +610,17 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon } } +//Xcode 16 +#if canImport(ContactProvider) +extension SolidRoundedButtonView: @retroactive TGPhotoSolidRoundedButtonView { + public func updateWidth(_ width: CGFloat) { + let _ = self.updateLayout(width: width, transition: .immediate) + } +} +#else extension SolidRoundedButtonView: TGPhotoSolidRoundedButtonView { public func updateWidth(_ width: CGFloat) { let _ = self.updateLayout(width: width, transition: .immediate) } } +#endif diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index 06912f377d1..22bce71c7d9 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -369,7 +369,7 @@ public final class ListMessageFileItemNode: ListMessageNode { private var absoluteLocation: (CGRect, CGSize)? private var context: AccountContext? - private (set) var message: Message? + private(set) var message: Message? private var appliedItem: ListMessageItem? private var layoutParams: ListViewItemLayoutParams? diff --git a/submodules/LocationUI/Sources/LocationAnnotation.swift b/submodules/LocationUI/Sources/LocationAnnotation.swift index eb0aa0d039b..78f0375eb01 100644 --- a/submodules/LocationUI/Sources/LocationAnnotation.swift +++ b/submodules/LocationUI/Sources/LocationAnnotation.swift @@ -26,10 +26,10 @@ private func generateSmallBackgroundImage(color: UIColor) -> UIImage? { }) } -class LocationPinAnnotation: NSObject, MKAnnotation { +public class LocationPinAnnotation: NSObject, MKAnnotation { let context: AccountContext let theme: PresentationTheme - var coordinate: CLLocationCoordinate2D { + public var coordinate: CLLocationCoordinate2D { willSet { self.willChangeValue(forKey: "coordinate") } @@ -55,10 +55,10 @@ class LocationPinAnnotation: NSObject, MKAnnotation { var isSelf = false var selfPeer: EnginePeer? - var title: String? = "" - var subtitle: String? = "" + public var title: String? = "" + public var subtitle: String? = "" - init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer?) { + public init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer?) { self.context = context self.theme = theme self.location = nil @@ -71,7 +71,7 @@ class LocationPinAnnotation: NSObject, MKAnnotation { super.init() } - init(context: AccountContext, theme: PresentationTheme, location: TelegramMediaMap, queryId: Int64?, resultId: String?, forcedSelection: Bool = false) { + public init(context: AccountContext, theme: PresentationTheme, location: TelegramMediaMap, queryId: Int64?, resultId: String?, forcedSelection: Bool = false) { self.context = context self.theme = theme self.location = location @@ -84,7 +84,7 @@ class LocationPinAnnotation: NSObject, MKAnnotation { super.init() } - init(context: AccountContext, theme: PresentationTheme, message: EngineMessage, selfPeer: EnginePeer?, isSelf: Bool, heading: Int32?) { + public init(context: AccountContext, theme: PresentationTheme, message: EngineMessage, selfPeer: EnginePeer?, isSelf: Bool, heading: Int32?) { self.context = context self.theme = theme self.location = nil @@ -104,7 +104,7 @@ class LocationPinAnnotation: NSObject, MKAnnotation { super.init() } - var id: String { + public var id: String { if let message = self.message { return "\(message.id.id)" } else if let peer = self.peer { @@ -157,7 +157,7 @@ private func removePulseAnimations(layer: CALayer) { layer.removeAnimation(forKey: "pulse-opacity") } -class LocationPinAnnotationView: MKAnnotationView { +public class LocationPinAnnotationView: MKAnnotationView { let shadowNode: ASImageNode let pulseNode: ASImageNode let backgroundNode: ASImageNode @@ -178,17 +178,17 @@ class LocationPinAnnotationView: MKAnnotationView { var headingKvoToken: NSKeyValueObservation? - override class var layerClass: AnyClass { + override public class var layerClass: AnyClass { return LocationPinAnnotationLayer.self } - func setZPosition(_ zPosition: CGFloat?) { + public func setZPosition(_ zPosition: CGFloat?) { if let layer = self.layer as? LocationPinAnnotationLayer { layer.customZPosition = zPosition } } - init(annotation: LocationPinAnnotation) { + public init(annotation: LocationPinAnnotation) { self.shadowNode = ASImageNode() self.shadowNode.image = UIImage(bundleImageName: "Location/PinShadow") if let image = self.shadowNode.image { @@ -244,7 +244,7 @@ class LocationPinAnnotationView: MKAnnotationView { self.annotation = annotation } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -252,7 +252,7 @@ class LocationPinAnnotationView: MKAnnotationView { self.headingKvoToken?.invalidate() } - var defaultZPosition: CGFloat { + public var defaultZPosition: CGFloat { if let annotation = self.annotation as? LocationPinAnnotation { if annotation.forcedSelection { return 0.0 @@ -266,7 +266,7 @@ class LocationPinAnnotationView: MKAnnotationView { } } - override var annotation: MKAnnotation? { + override public var annotation: MKAnnotation? { didSet { if let annotation = self.annotation as? LocationPinAnnotation { if let message = annotation.message { @@ -363,14 +363,14 @@ class LocationPinAnnotationView: MKAnnotationView { } } - override func prepareForReuse() { + override public func prepareForReuse() { self.previousPeerId = nil self.smallNode.isHidden = true self.backgroundNode.isHidden = false self.appeared = false } - override func setSelected(_ selected: Bool, animated: Bool) { + override public func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) if let annotation = self.annotation as? LocationPinAnnotation { @@ -547,7 +547,7 @@ class LocationPinAnnotationView: MKAnnotationView { } var previousPeerId: EnginePeer.Id? - func setPeer(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) { + public func setPeer(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) { let avatarNode: AvatarNode if let currentAvatarNode = self.avatarNode { avatarNode = currentAvatarNode @@ -566,7 +566,7 @@ class LocationPinAnnotationView: MKAnnotationView { } } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if let labelNode = self.labelNode { @@ -589,7 +589,7 @@ class LocationPinAnnotationView: MKAnnotationView { } var isRaised = false - func setRaised(_ raised: Bool, animated: Bool, completion: @escaping () -> Void = {}) { + public func setRaised(_ raised: Bool, animated: Bool, completion: @escaping () -> Void = {}) { guard raised != self.isRaised else { return } @@ -625,7 +625,7 @@ class LocationPinAnnotationView: MKAnnotationView { } } - func setCustom(_ custom: Bool, animated: Bool) { + public func setCustom(_ custom: Bool, animated: Bool) { if let annotation = self.annotation as? LocationPinAnnotation { self.iconNode.setSignal(venueIcon(engine: annotation.context.engine, type: "", background: false)) } @@ -676,7 +676,7 @@ class LocationPinAnnotationView: MKAnnotationView { self.dotNode.isHidden = !custom } - func animateAppearance() { + public func animateAppearance() { guard let annotation = self.annotation as? LocationPinAnnotation, annotation.location != nil && !annotation.forcedSelection else { return } @@ -694,7 +694,7 @@ class LocationPinAnnotationView: MKAnnotationView { } } - override func layoutSubviews() { + override public func layoutSubviews() { super.layoutSubviews() guard !self.animating else { diff --git a/submodules/LocationUI/Sources/LocationInfoListItem.swift b/submodules/LocationUI/Sources/LocationInfoListItem.swift index 1ce0bb28daa..4c3bca6ec83 100644 --- a/submodules/LocationUI/Sources/LocationInfoListItem.swift +++ b/submodules/LocationUI/Sources/LocationInfoListItem.swift @@ -11,7 +11,7 @@ import AppBundle import SolidRoundedButtonNode import ShimmerEffect -final class LocationInfoListItem: ListViewItem { +public final class LocationInfoListItem: ListViewItem { let presentationData: ItemListPresentationData let engine: TelegramEngine let location: TelegramMediaMap @@ -75,7 +75,7 @@ final class LocationInfoListItem: ListViewItem { } } -final class LocationInfoListItemNode: ListViewItemNode { +public final class LocationInfoListItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private var titleNode: TextNode? private var subtitleNode: TextNode? @@ -91,7 +91,7 @@ final class LocationInfoListItemNode: ListViewItemNode { private var layoutParams: ListViewItemLayoutParams? private var absoluteLocation: (CGRect, CGSize)? - required init() { + required public init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.buttonNode = HighlightableButtonNode() @@ -127,7 +127,7 @@ final class LocationInfoListItemNode: ListViewItemNode { self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } - override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = self.item { let makeLayout = self.asyncLayout() let (nodeLayout, nodeApply) = makeLayout(item, params) @@ -137,7 +137,7 @@ final class LocationInfoListItemNode: ListViewItemNode { } } - func asyncLayout() -> (_ item: LocationInfoListItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> (Signal?, (ListViewItemApply) -> Void)) { + public func asyncLayout() -> (_ item: LocationInfoListItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> (Signal?, (ListViewItemApply) -> Void)) { let currentItem = self.item let makeTitleLayout = TextNode.asyncLayout(self.titleNode) @@ -205,6 +205,8 @@ final class LocationInfoListItemNode: ListViewItemNode { strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor } + strongSelf.backgroundNode.isHidden = params.isStandalone + let arguments = VenueIconArguments(defaultBackgroundColor: item.presentationData.theme.chat.inputPanel.actionControlFillColor, defaultForegroundColor: item.presentationData.theme.chat.inputPanel.actionControlForegroundColor) if let updatedLocation = updatedLocation { strongSelf.venueIconNode.setSignal(venueIcon(engine: item.engine, type: updatedLocation.venue?.type ?? "", background: true)) @@ -384,11 +386,11 @@ final class LocationInfoListItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) } diff --git a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift index 44472fe5c26..8ed12e8b16f 100644 --- a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift +++ b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift @@ -34,7 +34,7 @@ private func generateShadowImage(theme: PresentationTheme, highlighted: Bool) -> })?.stretchableImage(withLeftCapWidth: 13, topCapHeight: 0) } -final class LocationMapHeaderNode: ASDisplayNode { +public final class LocationMapHeaderNode: ASDisplayNode { private var presentationData: PresentationData private let toggleMapModeSelection: () -> Void private let goToUserLocation: () -> Void @@ -44,8 +44,8 @@ final class LocationMapHeaderNode: ASDisplayNode { private var displayingPlacesButton = false private var proximityNotification: Bool? - let mapNode: LocationMapNode - var trackingMode: LocationTrackingMode = .none + public let mapNode: LocationMapNode + public var trackingMode: LocationTrackingMode = .none private let optionsBackgroundNode: ASImageNode private let optionsSeparatorNode: ASDisplayNode @@ -57,9 +57,9 @@ final class LocationMapHeaderNode: ASDisplayNode { private let placesButtonNode: HighlightableButtonNode private let shadowNode: ASImageNode - private var validLayout: (ContainerViewLayout, CGFloat, CGFloat, CGFloat, CGSize)? + private var validLayout: (ContainerViewLayout, CGFloat, CGFloat, CGFloat, CGFloat, CGSize)? - init(presentationData: PresentationData, toggleMapModeSelection: @escaping () -> Void, goToUserLocation: @escaping () -> Void, setupProximityNotification: @escaping (Bool) -> Void = { _ in }, showPlacesInThisArea: @escaping () -> Void = {}) { + public init(presentationData: PresentationData, toggleMapModeSelection: @escaping () -> Void, goToUserLocation: @escaping () -> Void, setupProximityNotification: @escaping (Bool) -> Void = { _ in }, showPlacesInThisArea: @escaping () -> Void = {}) { self.presentationData = presentationData self.toggleMapModeSelection = toggleMapModeSelection self.goToUserLocation = goToUserLocation @@ -131,7 +131,7 @@ final class LocationMapHeaderNode: ASDisplayNode { self.placesButtonNode.addTarget(self, action: #selector(self.placesPressed), forControlEvents: .touchUpInside) } - func updateState(mapMode: LocationMapMode, trackingMode: LocationTrackingMode, displayingMapModeOptions: Bool, displayingPlacesButton: Bool, proximityNotification: Bool?, animated: Bool) { + public func updateState(mapMode: LocationMapMode, trackingMode: LocationTrackingMode, displayingMapModeOptions: Bool, displayingPlacesButton: Bool, proximityNotification: Bool?, animated: Bool) { self.mapNode.mapMode = mapMode self.trackingMode = trackingMode self.infoButtonNode.isSelected = displayingMapModeOptions @@ -143,13 +143,13 @@ final class LocationMapHeaderNode: ASDisplayNode { self.displayingPlacesButton = displayingPlacesButton self.proximityNotification = proximityNotification - if updateLayout, let (layout, navigationBarHeight, topPadding, offset, size) = self.validLayout { + if updateLayout, let (layout, navigationBarHeight, topPadding, controlsTopPadding, offset, size) = self.validLayout { let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .spring) : .immediate - self.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: topPadding, offset: offset, size: size, transition: transition) + self.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: topPadding, controlsTopPadding: controlsTopPadding, offset: offset, size: size, transition: transition) } } - func updatePresentationData(_ presentationData: PresentationData) { + public func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData self.optionsBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme) @@ -177,13 +177,13 @@ final class LocationMapHeaderNode: ASDisplayNode { } } - func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, topPadding: CGFloat, offset: CGFloat, size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayout = (layout, navigationBarHeight, topPadding, offset, size) + public func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, topPadding: CGFloat, controlsTopPadding: CGFloat, offset: CGFloat, size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (layout, navigationBarHeight, topPadding, controlsTopPadding, offset, size) - let mapHeight: CGFloat = floor(layout.size.height * 1.3) - let mapFrame = CGRect(x: 0.0, y: floorToScreenPixels((size.height - mapHeight + navigationBarHeight) / 2.0) + offset, width: size.width, height: mapHeight) + let mapHeight: CGFloat = floor(layout.size.height * 1.3) + layout.intrinsicInsets.top * 2.0 + let mapFrame = CGRect(x: 0.0, y: floorToScreenPixels((size.height - mapHeight + navigationBarHeight) / 2.0) + offset + floor(layout.intrinsicInsets.top * 0.5), width: size.width, height: mapHeight) transition.updateFrame(node: self.mapNode, frame: mapFrame) - self.mapNode.updateLayout(size: mapFrame.size) + self.mapNode.updateLayout(size: mapFrame.size, topPadding: 0.0) let inset: CGFloat = 6.0 @@ -191,6 +191,8 @@ final class LocationMapHeaderNode: ASDisplayNode { let placesButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - placesButtonSize.width) / 2.0), y: self.displayingPlacesButton ? navigationBarHeight + topPadding + inset : 0.0), size: placesButtonSize) transition.updateFrame(node: self.placesBackgroundNode, frame: placesButtonFrame) transition.updateFrame(node: self.placesButtonNode, frame: CGRect(origin: CGPoint(), size: placesButtonSize)) + transition.updateAlpha(node: self.placesBackgroundNode, alpha: self.displayingPlacesButton ? 1.0 : 0.0) + transition.updateAlpha(node: self.placesButtonNode, alpha: self.displayingPlacesButton ? 1.0 : 0.0) transition.updateFrame(node: self.shadowNode, frame: CGRect(x: 0.0, y: size.height - 14.0, width: size.width, height: 14.0)) @@ -207,26 +209,26 @@ final class LocationMapHeaderNode: ASDisplayNode { transition.updateAlpha(node: self.notificationButtonNode, alpha: self.proximityNotification != nil ? 1.0 : 0.0) transition.updateAlpha(node: self.optionsSecondSeparatorNode, alpha: self.proximityNotification != nil ? 1.0 : 0.0) - transition.updateFrame(node: self.optionsBackgroundNode, frame: CGRect(x: size.width - inset - panelButtonSize.width - panelInset * 2.0 - layout.safeInsets.right, y: navigationBarHeight + topPadding + inset, width: panelButtonSize.width + panelInset * 2.0, height: panelHeight + panelInset * 2.0)) + transition.updateFrame(node: self.optionsBackgroundNode, frame: CGRect(x: size.width - inset - panelButtonSize.width - panelInset * 2.0 - layout.safeInsets.right, y: navigationBarHeight + controlsTopPadding + inset, width: panelButtonSize.width + panelInset * 2.0, height: panelHeight + panelInset * 2.0)) let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) let optionsAlpha: CGFloat = size.height > 160.0 + navigationBarHeight && !self.forceIsHidden ? 1.0 : 0.0 alphaTransition.updateAlpha(node: self.optionsBackgroundNode, alpha: optionsAlpha) } - var forceIsHidden: Bool = false { + public var forceIsHidden: Bool = false { didSet { - if let (layout, navigationBarHeight, topPadding, offset, size) = self.validLayout { - self.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: topPadding, offset: offset, size: size, transition: .immediate) + if let (layout, navigationBarHeight, topPadding, controlsTopPadding, offset, size) = self.validLayout { + self.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: topPadding, controlsTopPadding: controlsTopPadding, offset: offset, size: size, transition: .immediate) } } } - func updateHighlight(_ highlighted: Bool) { + public func updateHighlight(_ highlighted: Bool) { self.shadowNode.image = generateShadowImage(theme: self.presentationData.theme, highlighted: highlighted) } - func proximityButtonFrame() -> CGRect? { + public func proximityButtonFrame() -> CGRect? { if self.notificationButtonNode.alpha > 0.0 { return self.optionsBackgroundNode.view.convert(self.notificationButtonNode.frame, to: self.view) } else { diff --git a/submodules/LocationUI/Sources/LocationMapNode.swift b/submodules/LocationUI/Sources/LocationMapNode.swift index def8ec68306..e2aa9886b97 100644 --- a/submodules/LocationUI/Sources/LocationMapNode.swift +++ b/submodules/LocationUI/Sources/LocationMapNode.swift @@ -5,8 +5,6 @@ import Display import SwiftSignalKit import MapKit -let defaultMapSpan = MKCoordinateSpan(latitudeDelta: 0.016, longitudeDelta: 0.016) -let viewMapSpan = MKCoordinateSpan(latitudeDelta: 0.008, longitudeDelta: 0.008) private let pinOffset = CGPoint(x: 0.0, y: 33.0) public enum LocationMapMode { @@ -128,7 +126,70 @@ private func generateProximityDim(size: CGSize) -> UIImage { })! } -final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { +protocol MKMapViewDelegateTarget: AnyObject { + func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) + func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) + func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) + func mapView(_ mapView: MKMapView, didFailToLocateUserWithError error: Error) + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? + func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) + func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) + func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) + func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer +} + +private final class MKMapViewDelegateImpl: NSObject, MKMapViewDelegate { + private weak var target: MKMapViewDelegateTarget? + + init(target: MKMapViewDelegateTarget) { + self.target = target + + super.init() + } + + func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { + self.target?.mapView(mapView, regionWillChangeAnimated: animated) + } + + func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { + self.target?.mapView(mapView, regionDidChangeAnimated: animated) + } + + func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) { + self.target?.mapView(mapView, didUpdate: userLocation) + } + + func mapView(_ mapView: MKMapView, didFailToLocateUserWithError error: Error) { + self.target?.mapView(mapView, didFailToLocateUserWithError: error) + } + + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + return self.target?.mapView(mapView, viewFor: annotation) + } + + func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) { + self.target?.mapView(mapView, didAdd: views) + } + + func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { + self.target?.mapView(mapView, didSelect: view) + } + + func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) { + self.target?.mapView(mapView, didDeselect: view) + } + + func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { + return self.target?.mapView(mapView, rendererFor: overlay) ?? MKOverlayRenderer() + } +} + +public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { + private var delegateImpl: MKMapViewDelegateImpl? + + public static let defaultMapSpan = MKCoordinateSpan(latitudeDelta: 0.016, longitudeDelta: 0.016) + public static let viewMapSpan = MKCoordinateSpan(latitudeDelta: 0.008, longitudeDelta: 0.008) + class ProximityCircleRenderer: MKCircleRenderer { override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) { super.draw(mapRect, zoomScale: zoomScale, in: context) @@ -204,7 +265,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } private var circleOverlay: MKCircle? - var activeProximityRadius: Double? { + public var activeProximityRadius: Double? { didSet { if let activeProximityRadius = self.activeProximityRadius { if let circleOverlay = self.circleOverlay { @@ -225,7 +286,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - override init() { + override public init() { self.pickerAnnotationContainerView = PickerAnnotationContainerView() self.pickerAnnotationContainerView.isHidden = true @@ -236,7 +297,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { }) } - override func didLoad() { + override public func didLoad() { super.didLoad() self.headingArrowView = UIImageView() @@ -255,7 +316,10 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { return self?.disableHorizontalTransitionGesture == true } - self.mapView?.delegate = self + let delegateImpl = MKMapViewDelegateImpl(target: self) + self.delegateImpl = delegateImpl + + self.mapView?.delegate = delegateImpl self.mapView?.mapType = self.mapMode.mapType self.mapView?.isRotateEnabled = self.isRotateEnabled self.mapView?.showsUserLocation = true @@ -292,7 +356,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - var trackingMode: LocationTrackingMode = .none { + public var trackingMode: LocationTrackingMode = .none { didSet { self.mapView?.userTrackingMode = self.trackingMode.userTrackingMode if self.trackingMode == .followWithHeading && self.headingArrowView?.image != nil { @@ -303,11 +367,18 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } + var topPadding: CGFloat = 0.0 var mapOffset: CGFloat = 0.0 - func setMapCenter(coordinate: CLLocationCoordinate2D, radius: Double, insets: UIEdgeInsets, offset: CGFloat, animated: Bool = false) { + var hasValidLayout: Bool = false + var pendingSetMapCenter: (coordinate: CLLocationCoordinate2D, span: MKCoordinateSpan, offset: CGPoint, isUserLocation: Bool, hidePicker: Bool, animated: Bool)? + + public func setMapCenter(coordinate: CLLocationCoordinate2D, radius: Double, insets: UIEdgeInsets, offset: CGFloat, animated: Bool = false) { self.mapOffset = offset self.ignoreRegionChanges = true + var insets = insets + insets.top += self.topPadding + let mapRect = MKMapRect(region: MKCoordinateRegion(center: coordinate, latitudinalMeters: radius * 2.0, longitudinalMeters: radius * 2.0)) self.mapView?.setVisibleMapRect(mapRect, edgePadding: insets, animated: animated) self.ignoreRegionChanges = false @@ -315,16 +386,34 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { self.proximityDimView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY + offset) } - func setMapCenter(coordinate: CLLocationCoordinate2D, span: MKCoordinateSpan = defaultMapSpan, offset: CGPoint = CGPoint(), isUserLocation: Bool = false, hidePicker: Bool = false, animated: Bool = false) { + public func setMapCenter(coordinate: CLLocationCoordinate2D, span: MKCoordinateSpan = defaultMapSpan, offset: CGPoint = CGPoint(), isUserLocation: Bool = false, hidePicker: Bool = false, animated: Bool = false) { + self.pendingSetMapCenter = ( + coordinate, span, offset, isUserLocation, hidePicker, animated + ) + + if self.hasValidLayout { + self.applyPendingSetMapCenter() + } + } + + private func applyPendingSetMapCenter() { + if !self.hasValidLayout { + return + } + guard let (coordinate, span, offset, isUserLocation, hidePicker, animated) = self.pendingSetMapCenter else { + return + } + self.pendingSetMapCenter = nil + let region = MKCoordinateRegion(center: coordinate, span: span) self.ignoreRegionChanges = true - if offset == CGPoint() { + if offset == CGPoint() && self.topPadding == 0.0 { self.mapView?.setRegion(region, animated: animated) } else { let mapRect = MKMapRect(region: region) - self.mapView?.setVisibleMapRect(mapRect, edgePadding: UIEdgeInsets(top: offset.y, left: offset.x, bottom: 0.0, right: 0.0), animated: animated) + self.mapView?.setVisibleMapRect(mapRect, edgePadding: UIEdgeInsets(top: offset.y + self.topPadding, left: offset.x, bottom: 0.0, right: 0.0), animated: animated) } - self.ignoreRegionChanges = false + self.ignoreRegionChanges = false if isUserLocation { if !self.returnedToUserLocation { @@ -339,7 +428,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { + public func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { guard !self.ignoreRegionChanges, let scrollView = mapView.subviews.first, let gestureRecognizers = scrollView.gestureRecognizers else { return } @@ -356,7 +445,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { + public func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { let wasDragging = self.isDragging if self.isDragging { self.isDragging = false @@ -372,7 +461,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) { + public func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) { guard let location = userLocation.location else { return } @@ -380,11 +469,11 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { self.locationPromise.set(.single(location)) } - func mapView(_ mapView: MKMapView, didFailToLocateUserWithError error: Error) { + public func mapView(_ mapView: MKMapView, didFailToLocateUserWithError error: Error) { self.locationPromise.set(.single(nil)) } - func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { if annotation === mapView.userLocation { return nil } @@ -400,7 +489,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { return nil } - func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) { + public func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) { for view in views { if view.annotation is MKUserLocation { self.defaultUserLocationAnnotation = view.annotation @@ -424,7 +513,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { + public func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { guard let annotation = view.annotation as? LocationPinAnnotation else { return } @@ -440,7 +529,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) { + public func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) { if let view = view as? LocationPinAnnotationView { Queue.mainQueue().after(0.2) { view.setZPosition(view.defaultZPosition) @@ -459,7 +548,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { + public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { if let circle = overlay as? MKCircle { let renderer = ProximityCircleRenderer(circle: circle) renderer.fillColor = .clear @@ -472,7 +561,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - var distancesToAllAnnotations: Signal<[Double], NoError> { + public var distancesToAllAnnotations: Signal<[Double], NoError> { let poll = Signal<[LocationPinAnnotation], NoError> { [weak self] subscriber in if let strongSelf = self { subscriber.putNext(strongSelf.annotations) @@ -497,30 +586,30 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - var currentUserLocation: CLLocation? { + public var currentUserLocation: CLLocation? { return self.mapView?.userLocation.location } - var userLocation: Signal { + public var userLocation: Signal { return .single(self.currentUserLocation) |> then (self.locationPromise.get()) } - var mapCenterCoordinate: CLLocationCoordinate2D? { + public var mapCenterCoordinate: CLLocationCoordinate2D? { guard let mapView = self.mapView else { return nil } return mapView.convert(CGPoint(x: (mapView.frame.width + pinOffset.x) / 2.0, y: (mapView.frame.height + pinOffset.y) / 2.0), toCoordinateFrom: mapView) } - var mapSpan: MKCoordinateSpan? { + public var mapSpan: MKCoordinateSpan? { guard let mapView = self.mapView else { return nil } return mapView.region.span } - func resetAnnotationSelection() { + public func resetAnnotationSelection() { guard let mapView = self.mapView else { return } @@ -529,8 +618,8 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - var pickerAnnotationView: LocationPinAnnotationView? = nil - var hasPickerAnnotation: Bool = false { + public var pickerAnnotationView: LocationPinAnnotationView? = nil + public var hasPickerAnnotation: Bool = false { didSet { if self.hasPickerAnnotation, let annotation = self.userLocationAnnotation { let pickerAnnotationView = LocationPinAnnotationView(annotation: annotation) @@ -544,7 +633,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - func switchToPicking(raise: Bool = false, animated: Bool) { + public func switchToPicking(raise: Bool = false, animated: Bool) { guard self.hasPickerAnnotation else { return } @@ -561,8 +650,8 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { self.resetScheduledPin() } - var customUserLocationAnnotationView: LocationPinAnnotationView? = nil - var userLocationAnnotation: LocationPinAnnotation? = nil { + public var customUserLocationAnnotationView: LocationPinAnnotationView? = nil + public var userLocationAnnotation: LocationPinAnnotation? = nil { didSet { if let annotation = self.userLocationAnnotation { self.customUserLocationAnnotationView?.removeFromSuperview() @@ -582,7 +671,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - var userHeading: CGFloat? = nil { + public var userHeading: CGFloat? = nil { didSet { if let heading = self.userHeading { self.headingArrowView?.isHidden = false @@ -594,7 +683,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - var annotations: [LocationPinAnnotation] = [] { + public var annotations: [LocationPinAnnotation] = [] { didSet { guard let mapView = self.mapView else { return @@ -709,7 +798,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { self.pinDisposable.set(nil) } - func showAll(animated: Bool = true) { + public func showAll(animated: Bool = true) { guard let mapView = self.mapView else { return } @@ -736,11 +825,17 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } - func updateLayout(size: CGSize) { - self.proximityDimView.frame = CGRect(origin: CGPoint(x: 0.0, y: self.mapOffset), size: size) + public func updateLayout(size: CGSize, topPadding: CGFloat) { + self.hasValidLayout = true + + self.topPadding = topPadding + + self.proximityDimView.frame = CGRect(origin: CGPoint(x: 0.0, y: self.topPadding + self.mapOffset), size: size) self.pickerAnnotationContainerView.frame = CGRect(x: 0.0, y: floorToScreenPixels((size.height - size.width) / 2.0), width: size.width, height: size.width) if let pickerAnnotationView = self.pickerAnnotationView { pickerAnnotationView.center = CGPoint(x: self.pickerAnnotationContainerView.frame.width / 2.0, y: self.pickerAnnotationContainerView.frame.height / 2.0) } + + self.applyPendingSetMapCenter() } } diff --git a/submodules/LocationUI/Sources/LocationOptionsNode.swift b/submodules/LocationUI/Sources/LocationOptionsNode.swift index 50fe8aae00d..3a7debafe25 100644 --- a/submodules/LocationUI/Sources/LocationOptionsNode.swift +++ b/submodules/LocationUI/Sources/LocationOptionsNode.swift @@ -6,14 +6,14 @@ import TelegramCore import TelegramPresentationData import SegmentedControlNode -final class LocationOptionsNode: ASDisplayNode { +public final class LocationOptionsNode: ASDisplayNode { private var presentationData: PresentationData private let backgroundNode: NavigationBackgroundNode private let separatorNode: ASDisplayNode private let segmentedControlNode: SegmentedControlNode - init(presentationData: PresentationData, updateMapMode: @escaping (LocationMapMode) -> Void) { + public init(presentationData: PresentationData, hasBackground: Bool = true, updateMapMode: @escaping (LocationMapMode) -> Void) { self.presentationData = presentationData self.backgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor) @@ -24,8 +24,11 @@ final class LocationOptionsNode: ASDisplayNode { super.init() - self.addSubnode(self.backgroundNode) - self.addSubnode(self.separatorNode) + if hasBackground { + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + } + self.addSubnode(self.segmentedControlNode) self.segmentedControlNode.selectedIndexChanged = { index in @@ -42,14 +45,14 @@ final class LocationOptionsNode: ASDisplayNode { } } - func updatePresentationData(_ presentationData: PresentationData) { + public func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData self.backgroundNode.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) self.separatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.presentationData.theme)) } - func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { + public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) self.backgroundNode.update(size: size, transition: transition) diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index 61f0ad4f4d5..df4d05e948a 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -18,7 +18,7 @@ public enum LocationPickerMode { } class LocationPickerInteraction { - let sendLocation: (CLLocationCoordinate2D, String?, String?) -> Void + let sendLocation: (CLLocationCoordinate2D, String?, MapGeoAddress?) -> Void let sendLiveLocation: (CLLocationCoordinate2D) -> Void let sendVenue: (TelegramMediaMap, Int64?, String?) -> Void let toggleMapModeSelection: () -> Void @@ -33,7 +33,7 @@ class LocationPickerInteraction { let openHomeWorkInfo: () -> Void let showPlacesInThisArea: () -> Void - init(sendLocation: @escaping (CLLocationCoordinate2D, String?, String?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, openHomeWorkInfo: @escaping () -> Void, showPlacesInThisArea: @escaping ()-> Void) { + init(sendLocation: @escaping (CLLocationCoordinate2D, String?, MapGeoAddress?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, openHomeWorkInfo: @escaping () -> Void, showPlacesInThisArea: @escaping ()-> Void) { self.sendLocation = sendLocation self.sendLiveLocation = sendLiveLocation self.sendVenue = sendVenue @@ -122,16 +122,27 @@ public final class LocationPickerController: ViewController, AttachmentContainab strongSelf.controllerNode.updatePresentationData(presentationData) } }) - - let locationWithTimeout: (CLLocationCoordinate2D, Int32?) -> TelegramMediaMap = { coordinate, timeout in - return TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: timeout, liveProximityNotificationRadius: nil) - } - - self.interaction = LocationPickerInteraction(sendLocation: { [weak self] coordinate, name, countryCode in + + self.interaction = LocationPickerInteraction(sendLocation: { [weak self] coordinate, name, geoAddress in guard let strongSelf = self else { return } - strongSelf.completion(locationWithTimeout(coordinate, nil), nil, nil, name, countryCode) + strongSelf.completion( + TelegramMediaMap( + latitude: coordinate.latitude, + longitude: coordinate.longitude, + heading: nil, + accuracyRadius: nil, + venue: nil, + address: geoAddress, + liveBroadcastingTimeout: nil, + liveProximityNotificationRadius: nil + ), + nil, + nil, + name, + geoAddress?.country + ) strongSelf.dismiss() }, sendLiveLocation: { [weak self] coordinate in guard let strongSelf = self else { @@ -190,7 +201,7 @@ public final class LocationPickerController: ViewController, AttachmentContainab } let venueType = venue.venue?.type ?? "" if ["home", "work"].contains(venueType) { - completion(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil), nil, nil, nil, nil) + completion(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil), nil, nil, nil, nil) } else { completion(venue, queryId, resultId, venue.venue?.address, nil) } diff --git a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift index 069066261f0..5e8ff2ca938 100644 --- a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift @@ -35,9 +35,15 @@ private enum LocationPickerEntryId: Hashable { case attribution } +private extension MapGeoAddress { + func withUpdated(street: String?) -> MapGeoAddress { + return MapGeoAddress(country: self.country, state: self.state, city: self.city, street: street) + } +} + private enum LocationPickerEntry: Comparable, Identifiable { - case city(PresentationTheme, String, String, TelegramMediaMap?, Int64?, String?, CLLocationCoordinate2D?, String?, String?) - case location(PresentationTheme, String, String, TelegramMediaMap?, Int64?, String?, CLLocationCoordinate2D?, String?, String?, Bool) + case city(PresentationTheme, String, String, TelegramMediaMap?, Int64?, String?, CLLocationCoordinate2D?, String?, MapGeoAddress?) + case location(PresentationTheme, String, String, TelegramMediaMap?, Int64?, String?, CLLocationCoordinate2D?, String?, MapGeoAddress?, Bool) case liveLocation(PresentationTheme, String, String, CLLocationCoordinate2D?) case header(PresentationTheme, String) case venue(PresentationTheme, TelegramMediaMap?, Int64?, String?, Int) @@ -62,20 +68,20 @@ private enum LocationPickerEntry: Comparable, Identifiable { static func ==(lhs: LocationPickerEntry, rhs: LocationPickerEntry) -> Bool { switch lhs { - case let .city(lhsTheme, lhsTitle, lhsSubtitle, lhsVenue, lhsQueryId, lhsResultId, lhsCoordinate, lhsName, lhsCountryCode): - if case let .city(rhsTheme, rhsTitle, rhsSubtitle, rhsVenue, rhsQueryId, rhsResultId, rhsCoordinate, rhsName, rhsCountryCode) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsVenue?.venue?.id == rhsVenue?.venue?.id, lhsQueryId == rhsQueryId && lhsResultId == rhsResultId, lhsCoordinate == rhsCoordinate, lhsName == rhsName, lhsCountryCode == rhsCountryCode { + case let .city(lhsTheme, lhsTitle, lhsSubtitle, lhsVenue, lhsQueryId, lhsResultId, lhsCoordinate, lhsName, lhsAddress): + if case let .city(rhsTheme, rhsTitle, rhsSubtitle, rhsVenue, rhsQueryId, rhsResultId, rhsCoordinate, rhsName, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsVenue?.venue?.id == rhsVenue?.venue?.id, lhsQueryId == rhsQueryId && lhsResultId == rhsResultId, locationCoordinatesAreEqual(lhsCoordinate, rhsCoordinate), lhsName == rhsName, lhsAddress == rhsAddress { return true } else { return false } - case let .location(lhsTheme, lhsTitle, lhsSubtitle, lhsVenue, lhsQueryId, lhsResultId, lhsCoordinate, lhsName, lhsCountryCode, lhsIsTop): - if case let .location(rhsTheme, rhsTitle, rhsSubtitle, rhsVenue, rhsQueryId, rhsResultId, rhsCoordinate, rhsName, rhsCountryCode, rhsIsTop) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsVenue?.venue?.id == rhsVenue?.venue?.id, lhsQueryId == rhsQueryId && lhsResultId == rhsResultId, lhsCoordinate == rhsCoordinate, lhsName == rhsName, lhsCountryCode == rhsCountryCode, lhsIsTop == rhsIsTop { + case let .location(lhsTheme, lhsTitle, lhsSubtitle, lhsVenue, lhsQueryId, lhsResultId, lhsCoordinate, lhsName, lhsAddress, lhsIsTop): + if case let .location(rhsTheme, rhsTitle, rhsSubtitle, rhsVenue, rhsQueryId, rhsResultId, rhsCoordinate, rhsName, rhsAddress, rhsIsTop) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsVenue?.venue?.id == rhsVenue?.venue?.id, lhsQueryId == rhsQueryId && lhsResultId == rhsResultId, locationCoordinatesAreEqual(lhsCoordinate, rhsCoordinate), lhsName == rhsName, lhsAddress == rhsAddress, lhsIsTop == rhsIsTop { return true } else { return false } case let .liveLocation(lhsTheme, lhsTitle, lhsSubtitle, lhsCoordinate): - if case let .liveLocation(rhsTheme, rhsTitle, rhsSubtitle, rhsCoordinate) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsCoordinate == rhsCoordinate { + if case let .liveLocation(rhsTheme, rhsTitle, rhsSubtitle, rhsCoordinate) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, locationCoordinatesAreEqual(lhsCoordinate, rhsCoordinate) { return true } else { return false @@ -147,21 +153,21 @@ private enum LocationPickerEntry: Comparable, Identifiable { func item(engine: TelegramEngine, presentationData: PresentationData, interaction: LocationPickerInteraction?) -> ListViewItem { switch self { - case let .city(_, title, subtitle, _, _, _, coordinate, name, countryCode): + case let .city(_, title, subtitle, _, _, _, coordinate, name, address): let icon: LocationActionListItemIcon if let name { - icon = .venue(TelegramMediaMap(latitude: 0, longitude: 0, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: name, address: presentationData.strings.Location_TypeCity, provider: "city", id: countryCode, type: "building/default"), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) + icon = .venue(TelegramMediaMap(latitude: 0, longitude: 0, heading: nil, accuracyRadius: nil, venue: MapVenue(title: name, address: presentationData.strings.Location_TypeCity, provider: "city", id: address?.country, type: "building/default"), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) } else { icon = .location } return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: engine, title: title, subtitle: subtitle, icon: icon, beginTimeAndTimeout: nil, action: { if let coordinate = coordinate { - interaction?.sendLocation(coordinate, name, countryCode) + interaction?.sendLocation(coordinate, name, address?.withUpdated(street: nil)) } }, highlighted: { highlighted in interaction?.updateSendActionHighlight(highlighted) }) - case let .location(_, title, subtitle, venue, queryId, resultId, coordinate, name, countryCode, isTop): + case let .location(_, title, subtitle, venue, queryId, resultId, coordinate, name, address, isTop): let icon: LocationActionListItemIcon if let venue = venue { icon = .venue(venue) @@ -172,7 +178,7 @@ private enum LocationPickerEntry: Comparable, Identifiable { if let venue = venue { interaction?.sendVenue(venue, queryId, resultId) } else if let coordinate = coordinate { - interaction?.sendLocation(coordinate, name, countryCode) + interaction?.sendLocation(coordinate, name, address) } }, highlighted: { highlighted in if isTop { @@ -240,7 +246,7 @@ enum LocationPickerLocation: Equatable { return false } case let .location(lhsCoordinate, lhsAddress): - if case let .location(rhsCoordinate, rhsAddress) = rhs, lhsCoordinate == rhsCoordinate, lhsAddress == rhsAddress { + if case let .location(rhsCoordinate, rhsAddress) = rhs, locationCoordinatesAreEqual(lhsCoordinate, rhsCoordinate), lhsAddress == rhsAddress { return true } else { return false @@ -260,9 +266,11 @@ struct LocationPickerState { var mapMode: LocationMapMode var displayingMapModeOptions: Bool var selectedLocation: LocationPickerLocation + var geoAddress: MapGeoAddress? var city: String? var street: String? var countryCode: String? + var state: String? var isStreet: Bool var forceSelection: Bool var searchingVenuesAround: Bool @@ -271,6 +279,7 @@ struct LocationPickerState { self.mapMode = .map self.displayingMapModeOptions = false self.selectedLocation = .none + self.geoAddress = nil self.city = nil self.street = nil self.isStreet = false @@ -474,10 +483,10 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM |> map { homeCoordinate, workCoordinate -> [TelegramMediaMap]? in var venues: [TelegramMediaMap] = [] if let (latitude, longitude) = homeCoordinate, let address = homeAddress { - venues.append(TelegramMediaMap(latitude: latitude, longitude: longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: presentationData.strings.Map_Home, address: address.displayString, provider: nil, id: "home", type: "home"), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) + venues.append(TelegramMediaMap(latitude: latitude, longitude: longitude, heading: nil, accuracyRadius: nil, venue: MapVenue(title: presentationData.strings.Map_Home, address: address.displayString, provider: nil, id: "home", type: "home"), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) } if let (latitude, longitude) = workCoordinate, let address = workAddress { - venues.append(TelegramMediaMap(latitude: latitude, longitude: longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: presentationData.strings.Map_Work, address: address.displayString, provider: nil, id: "work", type: "work"), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) + venues.append(TelegramMediaMap(latitude: latitude, longitude: longitude, heading: nil, accuracyRadius: nil, venue: MapVenue(title: presentationData.strings.Map_Work, address: address.displayString, provider: nil, id: "work", type: "work"), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) } return venues } @@ -535,7 +544,9 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM let foundVenues: Signal<([(TelegramMediaMap, String)], Int64, CLLocation)?, NoError> = .single(nil) |> then( self.searchVenuesPromise.get() - |> distinctUntilChanged + |> distinctUntilChanged(isEqual: { lhs, rhs in + return locationCoordinatesAreEqual(lhs, rhs) + }) |> mapToSignal { coordinate -> Signal<([(TelegramMediaMap, String)], Int64, CLLocation)?, NoError> in if let coordinate = coordinate { return (.single(nil) @@ -592,9 +603,9 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM } if source == .story { if state.street != "" { - entries.append(.location(presentationData.theme, state.street ?? presentationData.strings.Map_Locating, state.isStreet ? presentationData.strings.Location_TypeStreet : presentationData.strings.Location_TypeLocation, nil, nil, nil, coordinate, state.street, nil, false)) + entries.append(.location(presentationData.theme, state.street ?? presentationData.strings.Map_Locating, state.isStreet ? presentationData.strings.Location_TypeStreet : presentationData.strings.Location_TypeLocation, nil, nil, nil, coordinate, state.street, state.geoAddress, false)) } else if state.city != "" { - entries.append(.city(presentationData.theme, state.city ?? presentationData.strings.Map_Locating, presentationData.strings.Location_TypeCity, nil, nil, nil, coordinate, state.city, state.countryCode)) + entries.append(.city(presentationData.theme, state.city ?? presentationData.strings.Map_Locating, presentationData.strings.Location_TypeCity, nil, nil, nil, coordinate, state.city, state.geoAddress)) } } else { entries.append(.location(presentationData.theme, title, address ?? presentationData.strings.Map_Locating, nil, nil, nil, coordinate, state.street, nil, true)) @@ -641,10 +652,10 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM } if source == .story { if state.city != "" { - entries.append(.city(presentationData.theme, state.city ?? presentationData.strings.Map_Locating, presentationData.strings.Location_TypeCity, nil, nil, nil, coordinate, state.city, state.countryCode)) + entries.append(.city(presentationData.theme, state.city ?? presentationData.strings.Map_Locating, presentationData.strings.Location_TypeCity, nil, nil, nil, coordinate, state.city, state.geoAddress)) } if state.street != "" { - entries.append(.location(presentationData.theme, state.street ?? presentationData.strings.Map_Locating, state.isStreet ? presentationData.strings.Location_TypeStreet : presentationData.strings.Location_TypeLocation, nil, nil, nil, coordinate, state.street, nil, false)) + entries.append(.location(presentationData.theme, state.street ?? presentationData.strings.Map_Locating, state.isStreet ? presentationData.strings.Location_TypeStreet : presentationData.strings.Location_TypeLocation, nil, nil, nil, coordinate, state.street, state.geoAddress, false)) } } else { entries.append(.location(presentationData.theme, title, (userLocation?.horizontalAccuracy).flatMap { presentationData.strings.Map_AccurateTo(stringForDistance(strings: presentationData.strings, distance: $0)).string } ?? presentationData.strings.Map_Locating, nil, nil, nil, coordinate, state.street, nil, true)) @@ -717,7 +728,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM case .none, .venue: updateMap = true case let .location(previousCoordinate, _): - if previousCoordinate != coordinate { + if !locationCoordinatesAreEqual(previousCoordinate, coordinate) { updateMap = true } default: @@ -787,10 +798,15 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM } let locale = localeWithStrings(presentationData.strings) - if case let .location(coordinate, address) = state.selectedLocation, address == nil { - strongSelf.geocodingDisposable.set((reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude, locale: locale) - |> deliverOnMainQueue).start(next: { [weak self] placemark in - if let strongSelf = self { + let enLocale = Locale(identifier: "en-US") + + let setupGeocoding: (CLLocationCoordinate2D, @escaping (MapGeoAddress?, String, String?, String?, String?, Bool) -> Void) -> Void = { coordinate, completion in + strongSelf.geocodingDisposable.set( + combineLatest( + queue: Queue.mainQueue(), + reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude, locale: locale), + reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude, locale: enLocale) + ).start(next: { placemark, enPlacemark in var address = placemark?.fullAddress ?? "" if address.isEmpty { address = presentationData.strings.Map_Unknown @@ -823,65 +839,43 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM if streetName == "" && cityName == "" { streetName = presentationData.strings.Location_TypeLocation } - strongSelf.updateState { state in - var state = state - state.selectedLocation = .location(coordinate, address) - state.city = cityName - state.street = streetName - state.countryCode = countryCode - state.isStreet = placemark?.street != nil - return state + + var mapGeoAddress: MapGeoAddress? + if let countryCode, let enPlacemark { + mapGeoAddress = MapGeoAddress(country: countryCode, state: enPlacemark.state, city: enPlacemark.city, street: enPlacemark.street) } + completion(mapGeoAddress, address, cityName, streetName, countryCode, placemark?.street != nil) + } + )) + } + + if case let .location(coordinate, address) = state.selectedLocation, address == nil { + setupGeocoding(coordinate, { [weak self] geoAddress, address, cityName, streetName, countryCode, isStreet in + self?.updateState { state in + var state = state + state.selectedLocation = .location(coordinate, address) + state.geoAddress = geoAddress + state.city = cityName + state.street = streetName + state.countryCode = countryCode + state.isStreet = isStreet + return state } - })) + }) } else { let coordinate = controller.initialLocation ?? userLocation?.coordinate if case .none = state.selectedLocation, let coordinate, state.city == nil { - strongSelf.geocodingDisposable.set((reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude, locale: locale) - |> deliverOnMainQueue).start(next: { [weak self] placemark in - if let strongSelf = self { - var address = placemark?.fullAddress ?? "" - if address.isEmpty { - address = presentationData.strings.Map_Unknown - } - var cityName: String? - var streetName: String? - let countryCode = placemark?.countryCode - if let city = placemark?.city { - if let countryCode = placemark?.countryCode { - cityName = "\(city), \(displayCountryName(countryCode, locale: locale))" - } else { - cityName = city - } - } else { - cityName = "" - } - if let street = placemark?.street { - if let city = placemark?.city { - streetName = "\(street), \(city)" - } else { - streetName = street - } - } else if let name = placemark?.name { - streetName = name - } else if let country = placemark?.country, cityName == "" { - streetName = country - } else { - streetName = "" - } - if streetName == "" && cityName == "" { - streetName = presentationData.strings.Location_TypeLocation - } - strongSelf.updateState { state in - var state = state - state.city = cityName - state.street = streetName - state.countryCode = countryCode - state.isStreet = placemark?.street != nil - return state - } + setupGeocoding(coordinate, { [weak self] geoAddress, address, cityName, streetName, countryCode, isStreet in + self?.updateState { state in + var state = state + state.geoAddress = geoAddress + state.city = cityName + state.street = streetName + state.countryCode = countryCode + state.isStreet = isStreet + return state } - })) + }) } else { strongSelf.geocodingDisposable.set(nil) } @@ -908,7 +902,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM strongSelf.listOffset = max(0.0, offset) let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(0.0, offset + overlap))) listTransition.updateFrame(node: strongSelf.headerNode, frame: headerFrame) - strongSelf.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, offset: 0.0, size: headerFrame.size, transition: listTransition) + strongSelf.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, controlsTopPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, offset: 0.0, size: headerFrame.size, transition: listTransition) strongSelf.layoutEmptyResultsPlaceholder(transition: listTransition) } @@ -1122,7 +1116,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: headerHeight)) transition.updateFrame(node: self.headerNode, frame: headerFrame) - self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationHeight, topPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, offset: 0.0, size: headerFrame.size, transition: transition) + self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationHeight, topPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, controlsTopPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, offset: 0.0, size: headerFrame.size, transition: transition) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) let scrollToItem: ListViewScrollToItem? diff --git a/submodules/LocationUI/Sources/LocationSearchContainerNode.swift b/submodules/LocationUI/Sources/LocationSearchContainerNode.swift index 1ae77009a7a..38f7848b769 100644 --- a/submodules/LocationUI/Sources/LocationSearchContainerNode.swift +++ b/submodules/LocationUI/Sources/LocationSearchContainerNode.swift @@ -194,7 +194,7 @@ final class LocationSearchContainerNode: ASDisplayNode { guard let placemarkLocation = placemark.location else { continue } - let location = TelegramMediaMap(latitude: placemarkLocation.coordinate.latitude, longitude: placemarkLocation.coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + let location = TelegramMediaMap(latitude: placemarkLocation.coordinate.latitude, longitude: placemarkLocation.coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: location, queryId: nil, resultId: nil, title: placemark.name ?? "Name", distance: placemarkLocation.distance(from: currentLocation))) diff --git a/submodules/LocationUI/Sources/LocationUtils.swift b/submodules/LocationUI/Sources/LocationUtils.swift index 3abf13348be..df5d59f89dd 100644 --- a/submodules/LocationUI/Sources/LocationUtils.swift +++ b/submodules/LocationUI/Sources/LocationUtils.swift @@ -8,7 +8,7 @@ import AccountContext extension TelegramMediaMap { convenience init(coordinate: CLLocationCoordinate2D, liveBroadcastingTimeout: Int32? = nil, proximityNotificationRadius: Int32? = nil) { - self.init(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: liveBroadcastingTimeout, liveProximityNotificationRadius: proximityNotificationRadius) + self.init(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: liveBroadcastingTimeout, liveProximityNotificationRadius: proximityNotificationRadius) } var coordinate: CLLocationCoordinate2D { @@ -24,12 +24,20 @@ extension MKMapRect { } } -extension CLLocationCoordinate2D: Equatable { - +public func locationCoordinatesAreEqual(_ lhs: CLLocationCoordinate2D?, _ rhs: CLLocationCoordinate2D?) -> Bool { + if let lhs, let rhs { + return lhs.isEqual(to: rhs) + } else if (lhs == nil) != (rhs == nil) { + return false + } else { + return true + } } -public func ==(lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { - return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude +extension CLLocationCoordinate2D { + func isEqual(to other: CLLocationCoordinate2D) -> Bool { + return self.latitude == other.latitude && self.longitude == other.longitude + } } public func nearbyVenues(context: AccountContext, story: Bool = false, latitude: Double, longitude: Double, query: String? = nil) -> Signal { @@ -101,7 +109,7 @@ func stringForEstimatedDuration(strings: PresentationStrings, time: Double, form } } -func throttledUserLocation(_ userLocation: Signal) -> Signal { +public func throttledUserLocation(_ userLocation: Signal) -> Signal { return userLocation |> reduceLeft(value: nil) { current, updated, emit -> CLLocation? in if let current = current { @@ -126,13 +134,13 @@ func throttledUserLocation(_ userLocation: Signal) -> Sign } } -enum ExpectedTravelTime: Equatable { +public enum ExpectedTravelTime: Equatable { case unknown case calculating case ready(Double) } -func getExpectedTravelTime(coordinate: CLLocationCoordinate2D, transportType: MKDirectionsTransportType) -> Signal { +public func getExpectedTravelTime(coordinate: CLLocationCoordinate2D, transportType: MKDirectionsTransportType) -> Signal { return Signal { subscriber in subscriber.putNext(.calculating) diff --git a/submodules/LocationUI/Sources/LocationViewControllerNode.swift b/submodules/LocationUI/Sources/LocationViewControllerNode.swift index 752e0a5f370..8a5ebd03ece 100644 --- a/submodules/LocationUI/Sources/LocationViewControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationViewControllerNode.swift @@ -41,159 +41,159 @@ private struct LocationViewTransaction { let animated: Bool } -private enum LocationViewEntryId: Hashable { +public enum LocationViewEntryId: Hashable { case info case toggleLiveLocation(Bool) case liveLocation(UInt32) } -private enum LocationViewEntry: Comparable, Identifiable { +public enum LocationViewEntry: Comparable, Identifiable { case info(PresentationTheme, TelegramMediaMap, String?, Double?, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime, Bool) case toggleLiveLocation(PresentationTheme, String, String, Double?, Double?, Bool, EngineMessage.Id?) case liveLocation(PresentationTheme, PresentationDateTimeFormat, PresentationPersonNameOrder, EngineMessage, Double?, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime, Int) - var stableId: LocationViewEntryId { + public var stableId: LocationViewEntryId { switch self { - case .info: - return .info - case let .toggleLiveLocation(_, _, _, _, _, additional, _): - return .toggleLiveLocation(additional) - case let .liveLocation(_, _, _, message, _, _, _, _, _): - return .liveLocation(message.stableId) + case .info: + return .info + case let .toggleLiveLocation(_, _, _, _, _, additional, _): + return .toggleLiveLocation(additional) + case let .liveLocation(_, _, _, message, _, _, _, _, _): + return .liveLocation(message.stableId) } } - static func ==(lhs: LocationViewEntry, rhs: LocationViewEntry) -> Bool { + public static func ==(lhs: LocationViewEntry, rhs: LocationViewEntry) -> Bool { switch lhs { - case let .info(lhsTheme, lhsLocation, lhsAddress, lhsDistance, lhsDrivingTime, lhsTransitTime, lhsWalkingTime, lhsHasEta): - if case let .info(rhsTheme, rhsLocation, rhsAddress, rhsDistance, rhsDrivingTime, rhsTransitTime, rhsWalkingTime, rhsHasEta) = rhs, lhsTheme === rhsTheme, lhsLocation.venue?.id == rhsLocation.venue?.id, lhsAddress == rhsAddress, lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsTransitTime == rhsTransitTime, lhsWalkingTime == rhsWalkingTime, lhsHasEta == rhsHasEta { - return true - } else { - return false - } - case let .toggleLiveLocation(lhsTheme, lhsTitle, lhsSubtitle, lhsBeginTimestamp, lhsTimeout, lhsAdditional, lhsMessageId): - if case let .toggleLiveLocation(rhsTheme, rhsTitle, rhsSubtitle, rhsBeginTimestamp, rhsTimeout, rhsAdditional, rhsMessageId) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsBeginTimestamp == rhsBeginTimestamp, lhsTimeout == rhsTimeout, lhsAdditional == rhsAdditional, lhsMessageId == rhsMessageId { - return true - } else { - return false - } - case let .liveLocation(lhsTheme, lhsDateTimeFormat, lhsNameDisplayOrder, lhsMessage, lhsDistance, lhsDrivingTime, lhsTransitTime, lhsWalkingTime, lhsIndex): - if case let .liveLocation(rhsTheme, rhsDateTimeFormat, rhsNameDisplayOrder, rhsMessage, rhsDistance, rhsDrivingTime, rhsTransitTime, rhsWalkingTime, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, areMessagesEqual(lhsMessage, rhsMessage), lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsTransitTime == rhsTransitTime, lhsWalkingTime == rhsWalkingTime, lhsIndex == rhsIndex { - return true - } else { - return false - } + case let .info(lhsTheme, lhsLocation, lhsAddress, lhsDistance, lhsDrivingTime, lhsTransitTime, lhsWalkingTime, lhsHasEta): + if case let .info(rhsTheme, rhsLocation, rhsAddress, rhsDistance, rhsDrivingTime, rhsTransitTime, rhsWalkingTime, rhsHasEta) = rhs, lhsTheme === rhsTheme, lhsLocation.venue?.id == rhsLocation.venue?.id, lhsAddress == rhsAddress, lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsTransitTime == rhsTransitTime, lhsWalkingTime == rhsWalkingTime, lhsHasEta == rhsHasEta { + return true + } else { + return false + } + case let .toggleLiveLocation(lhsTheme, lhsTitle, lhsSubtitle, lhsBeginTimestamp, lhsTimeout, lhsAdditional, lhsMessageId): + if case let .toggleLiveLocation(rhsTheme, rhsTitle, rhsSubtitle, rhsBeginTimestamp, rhsTimeout, rhsAdditional, rhsMessageId) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsBeginTimestamp == rhsBeginTimestamp, lhsTimeout == rhsTimeout, lhsAdditional == rhsAdditional, lhsMessageId == rhsMessageId { + return true + } else { + return false + } + case let .liveLocation(lhsTheme, lhsDateTimeFormat, lhsNameDisplayOrder, lhsMessage, lhsDistance, lhsDrivingTime, lhsTransitTime, lhsWalkingTime, lhsIndex): + if case let .liveLocation(rhsTheme, rhsDateTimeFormat, rhsNameDisplayOrder, rhsMessage, rhsDistance, rhsDrivingTime, rhsTransitTime, rhsWalkingTime, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, areMessagesEqual(lhsMessage, rhsMessage), lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsTransitTime == rhsTransitTime, lhsWalkingTime == rhsWalkingTime, lhsIndex == rhsIndex { + return true + } else { + return false + } } } - static func <(lhs: LocationViewEntry, rhs: LocationViewEntry) -> Bool { + public static func <(lhs: LocationViewEntry, rhs: LocationViewEntry) -> Bool { switch lhs { + case .info: + switch rhs { case .info: - switch rhs { - case .info: - return false - case .toggleLiveLocation, .liveLocation: - return true - } - case let .toggleLiveLocation(_, _, _, _, _, lhsAdditional, _): - switch rhs { - case .info: - return false - case let .toggleLiveLocation(_, _, _, _, _, rhsAdditional, _): - return !lhsAdditional && rhsAdditional - case .liveLocation: - return true - } - case let .liveLocation(_, _, _, _, _, _, _, _, lhsIndex): - switch rhs { - case .info, .toggleLiveLocation: - return false - case let .liveLocation(_, _, _, _, _, _, _, _, rhsIndex): - return lhsIndex < rhsIndex - } + return false + case .toggleLiveLocation, .liveLocation: + return true + } + case let .toggleLiveLocation(_, _, _, _, _, lhsAdditional, _): + switch rhs { + case .info: + return false + case let .toggleLiveLocation(_, _, _, _, _, rhsAdditional, _): + return !lhsAdditional && rhsAdditional + case .liveLocation: + return true + } + case let .liveLocation(_, _, _, _, _, _, _, _, lhsIndex): + switch rhs { + case .info, .toggleLiveLocation: + return false + case let .liveLocation(_, _, _, _, _, _, _, _, rhsIndex): + return lhsIndex < rhsIndex + } } } func item(context: AccountContext, presentationData: PresentationData, interaction: LocationViewInteraction?) -> ListViewItem { switch self { - case let .info(_, location, address, distance, drivingTime, transitTime, walkingTime, hasEta): - let addressString: String? - if let address = address { - addressString = address - } else { - addressString = presentationData.strings.Map_Locating - } - let distanceString: String? - if let distance = distance { - distanceString = distance < 10 ? presentationData.strings.Map_YouAreHere : presentationData.strings.Map_DistanceAway(stringForDistance(strings: presentationData.strings, distance: distance)).string - } else { - distanceString = nil - } - return LocationInfoListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, location: location, address: addressString, distance: distanceString, drivingTime: drivingTime, transitTime: transitTime, walkingTime: walkingTime, hasEta: hasEta, action: { - interaction?.goToCoordinate(location.coordinate) - }, drivingAction: { - interaction?.requestDirections(location, nil, .driving) - }, transitAction: { - interaction?.requestDirections(location, nil, .transit) - }, walkingAction: { - interaction?.requestDirections(location, nil, .walking) - }) - case let .toggleLiveLocation(_, title, subtitle, beginTimstamp, timeout, additional, messageId): - var beginTimeAndTimeout: (Double, Double)? - if let beginTimstamp = beginTimstamp, let timeout = timeout { - beginTimeAndTimeout = (beginTimstamp, timeout) - } else { - beginTimeAndTimeout = nil - } + case let .info(_, location, address, distance, drivingTime, transitTime, walkingTime, hasEta): + let addressString: String? + if let address = address { + addressString = address + } else { + addressString = presentationData.strings.Map_Locating + } + let distanceString: String? + if let distance = distance { + distanceString = distance < 10 ? presentationData.strings.Map_YouAreHere : presentationData.strings.Map_DistanceAway(stringForDistance(strings: presentationData.strings, distance: distance)).string + } else { + distanceString = nil + } + return LocationInfoListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, location: location, address: addressString, distance: distanceString, drivingTime: drivingTime, transitTime: transitTime, walkingTime: walkingTime, hasEta: hasEta, action: { + interaction?.goToCoordinate(location.coordinate) + }, drivingAction: { + interaction?.requestDirections(location, nil, .driving) + }, transitAction: { + interaction?.requestDirections(location, nil, .transit) + }, walkingAction: { + interaction?.requestDirections(location, nil, .walking) + }) + case let .toggleLiveLocation(_, title, subtitle, beginTimstamp, timeout, additional, messageId): + var beginTimeAndTimeout: (Double, Double)? + if let beginTimstamp = beginTimstamp, let timeout = timeout { + beginTimeAndTimeout = (beginTimstamp, timeout) + } else { + beginTimeAndTimeout = nil + } - let icon: LocationActionListItemIcon - if let timeout, Int32(timeout) != liveLocationIndefinitePeriod, !additional { - icon = .extendLiveLocation - } else if beginTimeAndTimeout != nil { - icon = .stopLiveLocation - } else { - icon = .liveLocation - } + let icon: LocationActionListItemIcon + if let timeout, Int32(timeout) != liveLocationIndefinitePeriod, !additional { + icon = .extendLiveLocation + } else if beginTimeAndTimeout != nil { + icon = .stopLiveLocation + } else { + icon = .liveLocation + } - return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, title: title, subtitle: subtitle, icon: icon, beginTimeAndTimeout: !additional ? beginTimeAndTimeout : nil, action: { - if beginTimeAndTimeout != nil { - if let timeout, Int32(timeout) != liveLocationIndefinitePeriod { - if additional { - interaction?.stopLiveLocation() - } else { - interaction?.sendLiveLocation(nil, true, messageId) - } - } else { + return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, title: title, subtitle: subtitle, icon: icon, beginTimeAndTimeout: !additional ? beginTimeAndTimeout : nil, action: { + if beginTimeAndTimeout != nil { + if let timeout, Int32(timeout) != liveLocationIndefinitePeriod { + if additional { interaction?.stopLiveLocation() + } else { + interaction?.sendLiveLocation(nil, true, messageId) } } else { - interaction?.sendLiveLocation(nil, false, nil) + interaction?.stopLiveLocation() } - }, highlighted: { highlight in - interaction?.updateSendActionHighlight(highlight) - }) - case let .liveLocation(_, dateTimeFormat, nameDisplayOrder, message, distance, drivingTime, transitTime, walkingTime, _): - var title: String? - if let author = message.author { - title = author.displayTitle(strings: presentationData.strings, displayOrder: nameDisplayOrder) + } else { + interaction?.sendLiveLocation(nil, false, nil) } - return LocationLiveListItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: context, message: message, distance: distance, drivingTime: drivingTime, transitTime: transitTime, walkingTime: walkingTime, action: { - if let location = getLocation(from: message) { - interaction?.goToCoordinate(location.coordinate) - } - }, longTapAction: {}, drivingAction: { - if let location = getLocation(from: message) { - interaction?.requestDirections(location, title, .driving) - } - }, transitAction: { - if let location = getLocation(from: message) { - interaction?.requestDirections(location, title, .transit) - } - }, walkingAction: { - if let location = getLocation(from: message) { - interaction?.requestDirections(location, title, .walking) - } - }) + }, highlighted: { highlight in + interaction?.updateSendActionHighlight(highlight) + }) + case let .liveLocation(_, dateTimeFormat, nameDisplayOrder, message, distance, drivingTime, transitTime, walkingTime, _): + var title: String? + if let author = message.author { + title = author.displayTitle(strings: presentationData.strings, displayOrder: nameDisplayOrder) + } + return LocationLiveListItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: context, message: message, distance: distance, drivingTime: drivingTime, transitTime: transitTime, walkingTime: walkingTime, action: { + if let location = getLocation(from: message) { + interaction?.goToCoordinate(location.coordinate) + } + }, longTapAction: {}, drivingAction: { + if let location = getLocation(from: message) { + interaction?.requestDirections(location, title, .driving) + } + }, transitAction: { + if let location = getLocation(from: message) { + interaction?.requestDirections(location, title, .transit) + } + }, walkingAction: { + if let location = getLocation(from: message) { + interaction?.requestDirections(location, title, .walking) + } + }) } } } @@ -208,22 +208,51 @@ private func preparedTransition(from fromEntries: [LocationViewEntry], to toEntr return LocationViewTransaction(deletions: deletions, insertions: insertions, updates: updates, gotTravelTimes: gotTravelTimes, count: toEntries.count, animated: animated) } -enum LocationViewLocation: Equatable { +public enum LocationViewLocation: Equatable { case initial case user case coordinate(CLLocationCoordinate2D, Bool) case custom + + public static func ==(lhs: LocationViewLocation, rhs: LocationViewLocation) -> Bool { + switch lhs { + case .initial: + if case .initial = rhs { + return true + } else { + return false + } + case .user: + if case .user = rhs { + return true + } else { + return false + } + case let .coordinate(lhsCoordinate, lhsValue): + if case let .coordinate(rhsCoordinate, rhsValue) = rhs, locationCoordinatesAreEqual(lhsCoordinate, rhsCoordinate), lhsValue == rhsValue { + return true + } else { + return false + } + case .custom: + if case .custom = rhs { + return true + } else { + return false + } + } + } } -struct LocationViewState { - var mapMode: LocationMapMode - var displayingMapModeOptions: Bool - var selectedLocation: LocationViewLocation - var trackingMode: LocationTrackingMode - var updatingProximityRadius: Int32? - var cancellingProximityRadius: Bool +public struct LocationViewState { + public var mapMode: LocationMapMode + public var displayingMapModeOptions: Bool + public var selectedLocation: LocationViewLocation + public var trackingMode: LocationTrackingMode + public var updatingProximityRadius: Int32? + public var cancellingProximityRadius: Bool - init() { + public init() { self.mapMode = .map self.displayingMapModeOptions = false self.selectedLocation = .initial @@ -614,12 +643,12 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan switch state.selectedLocation { case .initial: if previousState?.selectedLocation != .initial { - strongSelf.headerNode.mapNode.setMapCenter(coordinate: location.coordinate, span: viewMapSpan, animated: previousState != nil) + strongSelf.headerNode.mapNode.setMapCenter(coordinate: location.coordinate, span: LocationMapNode.viewMapSpan, animated: previousState != nil) } case let .coordinate(coordinate, defaultSpan): - if let previousState = previousState, case let .coordinate(previousCoordinate, _) = previousState.selectedLocation, previousCoordinate == coordinate { + if let previousState = previousState, case let .coordinate(previousCoordinate, _) = previousState.selectedLocation, locationCoordinatesAreEqual(previousCoordinate, coordinate) { } else { - strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, span: defaultSpan ? defaultMapSpan : viewMapSpan, animated: true) + strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, span: defaultSpan ? LocationMapNode.defaultMapSpan : LocationMapNode.viewMapSpan, animated: true) } case .user: if previousState?.selectedLocation != .user, let userLocation = userLocation { @@ -685,7 +714,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan strongSelf.listOffset = max(0.0, offset) let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(0.0, offset + overlap))) listTransition.updateFrame(node: strongSelf.headerNode, frame: headerFrame) - strongSelf.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, offset: 0.0, size: headerFrame.size, transition: listTransition) + strongSelf.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, controlsTopPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, offset: 0.0, size: headerFrame.size, transition: listTransition) } self.listNode.beganInteractiveDragging = { [weak self] _ in @@ -923,7 +952,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: headerHeight)) transition.updateFrame(node: self.headerNode, frame: headerFrame) - self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationHeight, topPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, offset: 0.0, size: headerFrame.size, transition: transition) + self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationHeight, topPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, controlsTopPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, offset: 0.0, size: headerFrame.size, transition: transition) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) diff --git a/submodules/LottieCpp/BUILD b/submodules/LottieCpp/BUILD new file mode 100644 index 00000000000..8a1b9b096f3 --- /dev/null +++ b/submodules/LottieCpp/BUILD @@ -0,0 +1,56 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +objc_library( + name = "LottieCpp", + enable_modules = True, + module_name = "LottieCpp", + srcs = glob([ + "lottiecpp/Sources/**/*.m", + "lottiecpp/Sources/**/*.mm", + "lottiecpp/Sources/**/*.h", + "lottiecpp/Sources/**/*.c", + "lottiecpp/Sources/**/*.cpp", + "lottiecpp/Sources/**/*.hpp", + "lottiecpp/PlatformSpecific/Darwin/Sources/**/*.m", + "lottiecpp/PlatformSpecific/Darwin/Sources/**/*.mm", + "lottiecpp/PlatformSpecific/Darwin/Sources/**/*.h", + "lottiecpp/PlatformSpecific/Darwin/Sources/**/*.c", + "lottiecpp/PlatformSpecific/Darwin/Sources/**/*.cpp", + "lottiecpp/PlatformSpecific/Darwin/Sources/**/*.hpp", + ]), + copts = [ + "-Werror", + "-I{}/lottiecpp/Sources".format(package_name()), + ], + hdrs = glob([ + "lottiecpp/PublicHeaders/**/*.h", + "lottiecpp/PlatformSpecific/Darwin/PublicHeaders/**/*.h", + ]), + includes = [ + "lottiecpp/PublicHeaders", + "lottiecpp/PlatformSpecific/Darwin/PublicHeaders", + ], + deps = [ + ], + sdk_frameworks = [ + "Foundation", + ], + visibility = [ + "//visibility:public", + ], +) + +cc_library( + name = "LottieCppBinding", + srcs = [], + hdrs = glob([ + "lottiecpp/PublicHeaders/**/*.h", + "lottiecpp/PlatformSpecific/Darwin/PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + ], + copts = [], + visibility = ["//visibility:public"], + linkstatic = 1, +) diff --git a/submodules/LottieCpp/lottiecpp b/submodules/LottieCpp/lottiecpp new file mode 160000 index 00000000000..b885e63e766 --- /dev/null +++ b/submodules/LottieCpp/lottiecpp @@ -0,0 +1 @@ +Subproject commit b885e63e766890d1cbf36b66cfe27cca55a6ec90 diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index 3d829fecb23..c5cab76799d 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -131,8 +131,15 @@ final class MediaPickerGridItemNode: GridItemNode { private var interaction: MediaPickerInteraction? private var theme: PresentationTheme? + private struct SelectionState: Equatable { + let selected: Bool + let index: Int? + let count: Int + } + private let selectionPromise = ValuePromise(SelectionState(selected: false, index: nil, count: 0)) private let spoilerDisposable = MetaDisposable() var spoilerNode: SpoilerOverlayNode? + var priceNode: PriceNode? private let progressDisposable = MetaDisposable() @@ -248,15 +255,18 @@ final class MediaPickerGridItemNode: GridItemNode { self.setNeedsLayout() } - if let interaction = self.interaction, let selectionState = interaction.selectionState { + if let interaction = self.interaction, let selectionState = interaction.selectionState { let selected = selectionState.isIdentifierSelected(self.identifier) + var selectionIndex: Int? if let selectableItem = self.selectableItem { let index = selectionState.index(of: selectableItem) if index != NSNotFound { self.checkNode?.content = .counter(Int(index)) + selectionIndex = Int(index) } } self.checkNode?.setSelected(selected, animated: animated) + self.selectionPromise.set(SelectionState(selected: selected, index: selectionIndex, count: selectionState.selectedItems().count)) } } @@ -284,7 +294,8 @@ final class MediaPickerGridItemNode: GridItemNode { self.typeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.draftNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - if animateSpoilerNode { + self.priceNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + if animateSpoilerNode || self.priceNode != nil { self.spoilerNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } } @@ -545,12 +556,27 @@ final class MediaPickerGridItemNode: GridItemNode { } } - self.spoilerDisposable.set((spoilerSignal - |> deliverOnMainQueue).start(next: { [weak self] hasSpoiler in + let priceSignal = Signal { subscriber in + if let signal = editingContext.priceSignal(forIdentifier: asset.localIdentifier) { + let disposable = signal.start(next: { next in + subscriber.putNext(next as? Int64) + }, error: { _ in + }, completed: nil)! + + return ActionDisposable { + disposable.dispose() + } + } else { + return EmptyDisposable + } + } + + self.spoilerDisposable.set((combineLatest(spoilerSignal, priceSignal, self.selectionPromise.get()) + |> deliverOnMainQueue).start(next: { [weak self] hasSpoiler, price, selectionState in guard let strongSelf = self else { return } - strongSelf.updateHasSpoiler(hasSpoiler) + strongSelf.updateHasSpoiler(hasSpoiler, price: selectionState.selected ? price : nil, isSingle: selectionState.count == 1 || selectionState.index == 1) })) if self.currentDraftState != nil { @@ -615,15 +641,17 @@ final class MediaPickerGridItemNode: GridItemNode { self.updateHiddenMedia() } + private var currentPrice: Int64? private var didSetupSpoiler = false - private func updateHasSpoiler(_ hasSpoiler: Bool) { + private func updateHasSpoiler(_ hasSpoiler: Bool, price: Int64?, isSingle: Bool) { var animated = true if !self.didSetupSpoiler { animated = false self.didSetupSpoiler = true } - - if hasSpoiler { + self.currentPrice = isSingle ? price : nil + + if hasSpoiler || price != nil { if self.spoilerNode == nil { let spoilerNode = SpoilerOverlayNode(enableAnimations: self.enableAnimations) self.insertSubnode(spoilerNode, aboveSubnode: self.imageNode) @@ -635,13 +663,40 @@ final class MediaPickerGridItemNode: GridItemNode { spoilerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } - self.spoilerNode?.update(size: self.bounds.size, transition: .immediate) - self.spoilerNode?.frame = CGRect(origin: .zero, size: self.bounds.size) + let bounds = self.bounds + self.spoilerNode?.update(size: bounds.size, transition: .immediate) + self.spoilerNode?.frame = CGRect(origin: .zero, size: bounds.size) + + if let price { + let priceNode: PriceNode + if let currentPriceNode = self.priceNode { + priceNode = currentPriceNode + } else { + priceNode = PriceNode() + if let spoilerNode = self.spoilerNode { + self.insertSubnode(priceNode, aboveSubnode: spoilerNode) + } + self.priceNode = priceNode + + if animated { + priceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + + self.priceNode?.update(size: bounds.size, price: isSingle ? price : nil, small: true, transition: .immediate) + } } else if let spoilerNode = self.spoilerNode { self.spoilerNode = nil spoilerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak spoilerNode] _ in spoilerNode?.removeFromSupernode() }) + + if let priceNode = self.priceNode { + self.priceNode = nil + priceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak priceNode] _ in + priceNode?.removeFromSupernode() + }) + } } } @@ -674,6 +729,11 @@ final class MediaPickerGridItemNode: GridItemNode { spoilerNode.update(size: self.bounds.size, transition: .immediate) } + if let priceNode = self.priceNode, self.bounds.width > 0.0 { + priceNode.frame = self.bounds + priceNode.update(size: self.bounds.size, price: self.currentPrice, small: true, transition: .immediate) + } + let statusSize = CGSize(width: 40.0, height: 40.0) if let statusNode = self.statusNode { statusNode.view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.bounds.width - statusSize.width) / 2.0), y: floorToScreenPixels((self.bounds.height - statusSize.height) / 2.0)), size: statusSize) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 375f4033cbd..57c0191e955 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -192,6 +192,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { private let bannedSendPhotos: (Int32, Bool)? private let bannedSendVideos: (Int32, Bool)? private let canBoostToUnrestrict: Bool + private let paidMediaAllowed: Bool private let subject: Subject private let saveEditedPhotos: Bool @@ -1754,6 +1755,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { bannedSendPhotos: (Int32, Bool)? = nil, bannedSendVideos: (Int32, Bool)? = nil, canBoostToUnrestrict: Bool = false, + paidMediaAllowed: Bool = false, subject: Subject, editingContext: TGMediaEditingContext? = nil, selectionContext: TGMediaSelectionContext? = nil, @@ -1773,6 +1775,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.bannedSendPhotos = bannedSendPhotos self.bannedSendVideos = bannedSendVideos self.canBoostToUnrestrict = canBoostToUnrestrict + self.paidMediaAllowed = paidMediaAllowed self.subject = subject self.saveEditedPhotos = saveEditedPhotos self.mainButtonState = mainButtonState @@ -1842,6 +1845,21 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } + if let selectionContext = self.interaction?.selectionState, let editingContext = self.interaction?.editingState { + var price: Int64? + for case let item as TGMediaEditableItem in selectionContext.selectedItems() { + if price == nil, let itemPrice = editingContext.price(for: item) as? Int64 { + price = itemPrice + break + } + } + + if let price, let item = item as? TGMediaEditableItem { + editingContext.setPrice(NSNumber(value: price), for: item) + } + } + + return true } @@ -2017,7 +2035,6 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.updateSelectionState(count: Int32(selectionContext.count())) - if case let .assets(_, mode) = self.subject, case .createSticker = mode { let _ = cutoutAvailability(context: context).startStandalone() } @@ -2493,9 +2510,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } var hasSpoilers = false + var price: Int64? var hasGeneric = false if let selectionContext = self.interaction?.selectionState, let editingContext = self.interaction?.editingState { for case let item as TGMediaEditableItem in selectionContext.selectedItems() { + if price == nil, let itemPrice = editingContext.price(for: item) as? Int64 { + price = itemPrice + } if editingContext.spoiler(for: item) { hasSpoilers = true } else { @@ -2515,8 +2536,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { ) |> deliverOnMainQueue |> map { [weak self] grouped, isCaptionAboveMediaAvailable -> ContextController.Items in + guard let self else { + return ContextController.Items(content: .list([])) + } var items: [ContextMenuItem] = [] - if !hasSpoilers { + if !hasSpoilers && price == nil { items.append(.action(ContextMenuActionItem(text: selectionCount > 1 ? strings.Attachment_SendAsFiles : strings.Attachment_SendAsFile, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/File"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in @@ -2529,9 +2553,9 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { // MARK: Nicegram RoundedVideos if canSendAsRoundedVideo( currentItem: nil, - editingContext: self?.interaction?.editingState, - selectionContext: self?.interaction?.selectionState - ) { + editingContext: self.interaction?.editingState, + selectionContext: self.interaction?.selectionState + ), price == nil { items.append(.action(ContextMenuActionItem(text: NGRoundedVideos.Resources.buttonTitle(), icon: { theme in return generateTintedImage(image: NGRoundedVideos.Resources.buttonIcon(), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in @@ -2544,39 +2568,29 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } // - if selectionCount > 1 { - if !items.isEmpty { - items.append(.separator) - } - items.append(.action(ContextMenuActionItem(text: strings.Attachment_Grouped, icon: { theme in - if !grouped { - return nil - } - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + if selectionCount > 1, price == nil { + items.append(.action(ContextMenuActionItem(text: strings.Attachment_SendWithoutGrouping, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/GroupingOff"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) - self?.groupedValue = true - }))) - items.append(.action(ContextMenuActionItem(text: strings.Attachment_Ungrouped, icon: { theme in - if grouped { - return nil - } - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, f in - f(.default) - self?.groupedValue = false + self?.controllerNode.send(asFile: false, silently: false, scheduleTime: nil, animated: true, parameters: nil, completion: {}) }))) } - if isSpoilerAvailable || (selectionCount > 0 && isCaptionAboveMediaAvailable) { + + var isPaidAvailable = false + if self.paidMediaAllowed, selectionCount <= 10 { + isPaidAvailable = true + } + if isSpoilerAvailable || isPaidAvailable || (selectionCount > 0 && isCaptionAboveMediaAvailable) { if !items.isEmpty { items.append(.separator) } if isCaptionAboveMediaAvailable { var mediaCaptionIsAbove = false - if let interaction = self?.interaction { + if let interaction = self.interaction { mediaCaptionIsAbove = interaction.captionIsAboveMedia } @@ -2593,7 +2607,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } }))) } - if isSpoilerAvailable { + if isSpoilerAvailable && price == nil { items.append(.action(ContextMenuActionItem(text: hasGeneric ? strings.Attachment_EnableSpoiler : strings.Attachment_DisableSpoiler, icon: { _ in return nil }, iconAnimation: ContextMenuActionItem.IconAnimation( name: "anim_spoiler", loop: true @@ -2610,6 +2624,38 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } }))) } + if isPaidAvailable { + let title: String + let titleLayout: ContextMenuActionItemTextLayout + if let price { + title = strings.Attachment_Paid_EditPrice + titleLayout = .secondLineWithValue(strings.Attachment_Paid_EditPrice_Stars(Int32(price))) + } else { + title = strings.Attachment_Paid_Create + titleLayout = .singleLine + } + items.append(.action(ContextMenuActionItem(text: title, textLayout: titleLayout, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Paid"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + guard let self else { + return + } + + let controller = self.context.sharedContext.makeStarsAmountScreen(context: self.context, initialValue: price, completion: { [weak self] amount in + guard let strongSelf = self else { + return + } + if let selectionContext = strongSelf.interaction?.selectionState, let editingContext = strongSelf.interaction?.editingState { + selectionContext.selectionLimit = 10 + for case let item as TGMediaEditableItem in selectionContext.selectedItems() { + editingContext.setPrice(NSNumber(value: amount), for: item) + } + } + }) + self.parentController()?.push(controller) + }))) + } } return ContextController.Items(content: .list(items)) } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift index ae408cd4ed0..78caaba0fe9 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift @@ -130,12 +130,27 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { } } - self.spoilerDisposable.set((spoilerSignal - |> deliverOnMainQueue).start(next: { [weak self] hasSpoiler in + let priceSignal = Signal { subscriber in + if let signal = editingState.priceSignal(forIdentifier: asset.uniqueIdentifier) { + let disposable = signal.start(next: { next in + subscriber.putNext(next as? Int64) + }, error: { _ in + }, completed: nil)! + + return ActionDisposable { + disposable.dispose() + } + } else { + return EmptyDisposable + } + } + + self.spoilerDisposable.set((combineLatest(spoilerSignal, priceSignal) + |> deliverOnMainQueue).start(next: { [weak self] hasSpoiler, price in guard let strongSelf = self else { return } - strongSelf.updateHasSpoiler(hasSpoiler) + strongSelf.updateHasSpoiler(hasSpoiler, price: price) })) } @@ -163,7 +178,7 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { } private var didSetupSpoiler = false - private func updateHasSpoiler(_ hasSpoiler: Bool) { + private func updateHasSpoiler(_ hasSpoiler: Bool, price: Int64?) { var animated = true if !self.didSetupSpoiler { animated = false @@ -463,6 +478,82 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { } } +final class PriceNode: ASDisplayNode { + let backgroundNode: NavigationBackgroundNode + let iconNode: ASImageNode + let lockNode: ASImageNode + let labelNode: ImmediateTextNode + + override init() { + self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x000000, alpha: 0.35), enableBlur: true) + + self.lockNode = ASImageNode() + self.lockNode.displaysAsynchronously = false + self.lockNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Lock"), color: .white) + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.image = UIImage(bundleImageName: "Premium/Stars/StarSmall") + + self.labelNode = ImmediateTextNode() + + super.init() + + self.isUserInteractionEnabled = false + + self.addSubnode(self.backgroundNode) + self.backgroundNode.addSubnode(self.lockNode) + self.backgroundNode.addSubnode(self.iconNode) + self.backgroundNode.addSubnode(self.labelNode) + } + + func update(size: CGSize, price: Int64?, small: Bool, transition: ContainedViewLayoutTransition) { + var nodeSize = CGSize(width: 50.0, height: 34.0) + var labelSize: CGSize = .zero + + var backgroundTransition = transition + let labelTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + if let price { + self.labelNode.attributedText = NSAttributedString(string: "\(price)", font: Font.semibold(15.0), textColor: .white) + + labelSize = self.labelNode.updateLayout(CGSize(width: 240.0, height: 50.0)) + nodeSize.width = labelSize.width + 40.0 + + if self.labelNode.alpha != 1.0 && self.backgroundNode.frame.width > 0.0 { + backgroundTransition = labelTransition + } + + labelTransition.updateAlpha(node: self.labelNode, alpha: 1.0) + labelTransition.updateAlpha(node: self.lockNode, alpha: 0.0) + } else { + if self.labelNode.alpha != 0.0 && self.backgroundNode.frame.width > 0.0 { + backgroundTransition = labelTransition + } + + labelTransition.updateAlpha(node: self.labelNode, alpha: 0.0) + labelTransition.updateAlpha(node: self.lockNode, alpha: 1.0) + } + + + self.backgroundNode.update(size: nodeSize, cornerRadius: 17.0, transition: backgroundTransition) + backgroundTransition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: floor((size.width - nodeSize.width) / 2.0), y: floor((size.height - nodeSize.height) / 2.0)), size: nodeSize)) + + if let _ = price { + if let icon = self.iconNode.image { + self.iconNode.frame = CGRect(origin: CGPoint(x: 9.0 - UIScreenPixel, y: floor((nodeSize.height - icon.size.height) / 2.0)), size: icon.size) + } + self.labelNode.frame = CGRect(origin: CGPoint(x: 30.0, y: floor((nodeSize.height - labelSize.height) / 2.0)), size: labelSize) + } else { + if let icon = self.iconNode.image { + self.iconNode.frame = CGRect(origin: CGPoint(x: 9.0 - UIScreenPixel, y: floor((nodeSize.height - icon.size.height) / 2.0)), size: icon.size) + } + if let icon = self.lockNode.image { + self.lockNode.frame = CGRect(origin: CGPoint(x: 28.0, y: floor((nodeSize.height - icon.size.height) / 2.0)), size: icon.size) + } + } + } +} + private class MessageBackgroundNode: ASDisplayNode { private let backgroundWallpaperNode: ChatMessageBubbleBackdrop private let backgroundNode: ChatMessageBackground @@ -520,6 +611,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS private let scrollNode: ASScrollNode private var backgroundNodes: [Int: MessageBackgroundNode] = [:] private var itemNodes: [String: MediaPickerSelectedItemNode] = [:] + private var priceNodes: [Int: PriceNode] = [:] private var reorderFeedback: HapticFeedback? private var reorderNode: ReorderingItemNode? @@ -629,11 +721,11 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS for (_, backgroundNode) in strongSelf.backgroundNodes { backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.1) if strongSelf.isExternalPreview { - Transition.immediate.setScale(layer: backgroundNode.layer, scale: 0.001) + ComponentTransition.immediate.setScale(layer: backgroundNode.layer, scale: 0.001) transition.updateTransformScale(layer: backgroundNode.layer, scale: 1.0) } } - + for (identifier, itemNode) in strongSelf.itemNodes { if !strongSelf.isObscuredExternalPreview, let (transitionView, _, _) = strongSelf.getTransitionView(identifier) { itemNode.animateFrom(transitionView, transition: transition) @@ -648,6 +740,15 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS } } + for (_, priceNode) in strongSelf.priceNodes { + strongSelf.scrollNode.addSubnode(priceNode) + priceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.1) + if strongSelf.isExternalPreview { + ComponentTransition.immediate.setScale(layer: priceNode.layer, scale: 0.001) + transition.updateTransformScale(layer: priceNode.layer, scale: 1.0) + } + } + if let topNode = strongSelf.messageNodes?.first, !topNode.alpha.isZero { topNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.1) transition.animatePositionAdditive(layer: topNode.layer, offset: CGPoint(x: 0.0, y: -30.0)) @@ -675,6 +776,10 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS itemNode.layer.removeAllAnimations() } + for (_, priceNode) in strongSelf.priceNodes { + priceNode.layer.removeAllAnimations() + } + strongSelf.messageNodes?.first?.layer.removeAllAnimations() strongSelf.messageNodes?.last?.layer.removeAllAnimations() @@ -695,6 +800,13 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS } } + for (_, priceNode) in self.priceNodes { + priceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false) + if self.isExternalPreview { + transition.updateTransformScale(layer: priceNode.layer, scale: 0.001) + } + } + for (identifier, itemNode) in self.itemNodes { if !self.isObscuredExternalPreview, let (transitionView, maybeDustNode, completion) = self.getTransitionView(identifier) { itemNode.animateTo(transitionView, dustNode: maybeDustNode, transition: transition, completion: completion) @@ -719,7 +831,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS } } - func animateOutOnSend(transition: Transition) { + func animateOutOnSend(transition: ComponentTransition) { transition.setAlpha(view: self.view, alpha: 0.0) } @@ -780,11 +892,24 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS self.reorderFeedback = HapticFeedback() } self.reorderFeedback?.impact() + + let priceTransition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) + for (_, node) in self.priceNodes { + priceTransition.updateAlpha(node: node, alpha: 0.0) + } } private func endReordering(point: CGPoint?) { if let reorderNode = self.reorderNode { self.reorderNode = nil + + let completion = { + let priceTransition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) + for (_, node) in self.priceNodes { + node.supernode?.view.bringSubviewToFront(node.view) + priceTransition.updateAlpha(node: node, alpha: 1.0) + } + } if let itemNode = reorderNode.itemNode, let point = point { var targetNode: MediaPickerSelectedItemNode? @@ -800,11 +925,13 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS } reorderNode.animateCompletion(completion: { [weak reorderNode] in reorderNode?.removeFromSupernode() + completion() }) self.reorderFeedback?.tap() } else { reorderNode.removeFromSupernode() reorderNode.itemNode?.isHidden = false + completion() } } @@ -829,6 +956,8 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS let sideInset: CGFloat = 34.0 let boundingWidth = min(320.0, size.width - insets.left - insets.right - sideInset * 2.0) + var price: Int64? + var validIds: [String] = [] for item in items { guard let asset = item as? TGMediaEditableItem, let identifier = asset.uniqueIdentifier else { @@ -857,6 +986,10 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS } else { itemSizes.append(asset.originalSize ?? CGSize()) } + + if price == nil, let priceValue = self.interaction?.editingState.price(for: asset) as? Int64 { + price = priceValue + } } if !self.didSetReady { @@ -1047,6 +1180,31 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS } } + if let price { + let priceNode: PriceNode + if let current = self.priceNodes[groupIndex] { + priceNode = current + } else { + priceNode = PriceNode() + self.priceNodes[groupIndex] = priceNode + self.scrollNode.addSubnode(priceNode) + } + + if priceNode.frame.width.isZero { + itemTransition = .immediate + } + + let priceNodeFrame = groupRect + itemTransition.updatePosition(node: priceNode, position: priceNodeFrame.center) + itemTransition.updateBounds(node: priceNode, bounds: CGRect(origin: CGPoint(), size: priceNodeFrame.size)) + priceNode.update(size: priceNode.frame.size, price: price, small: false, transition: itemTransition) + } else if let priceNode = self.priceNodes[groupIndex] { + self.priceNodes[groupIndex] = nil + priceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak priceNode] _ in + priceNode?.removeFromSupernode() + }) + } + contentHeight += groupSize.height contentWidth = max(contentWidth, groupSize.width) groupIndex += 1 @@ -1054,7 +1212,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS if let dragNode = self.messageNodes?.last { transition.updateAlpha(node: dragNode, alpha: items.count > 1 ? 1.0 : 0.0) - transition.updateFrame(node: dragNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top + contentHeight + 1.0), size: dragNode.frame.size)) + transition.updateFrame(node: dragNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top + contentHeight + 9.0), size: dragNode.frame.size)) var dragNodeFrame = dragNode.frame dragNodeFrame.origin.y = size.height - dragNodeFrame.origin.y - dragNodeFrame.size.height @@ -1133,15 +1291,15 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS } } - func animateIn(transition: Transition) { + func animateIn(transition: ComponentTransition) { self.animateIn(transition: transition.containedViewLayoutTransition, initiated: {}, completion: {}) } - func animateOut(transition: Transition) { + func animateOut(transition: ComponentTransition) { self.animateOut(transition: transition.containedViewLayoutTransition, completion: {}) } - func update(containerSize: CGSize, transition: Transition) -> CGSize { + func update(containerSize: CGSize, transition: ComponentTransition) -> CGSize { if var validLayout = self.validLayout { validLayout.size = containerSize self.validLayout = validLayout diff --git a/submodules/MediaPlayer/Package.swift b/submodules/MediaPlayer/Package.swift index 35d1875d351..a35bec3fb07 100644 --- a/submodules/MediaPlayer/Package.swift +++ b/submodules/MediaPlayer/Package.swift @@ -4,13 +4,13 @@ import PackageDescription let package = Package( - name: "MediaPlayer", + name: "TelegramMediaPlayer", platforms: [.macOS(.v10_13)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( - name: "MediaPlayer", - targets: ["MediaPlayer"]), + name: "TelegramMediaPlayer", + targets: ["TelegramMediaPlayer"]), ], dependencies: [ .package(name: "TelegramCore", path: "../TelegramCore"), @@ -27,7 +27,7 @@ let package = Package( // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( - name: "MediaPlayer", + name: "TelegramMediaPlayer", dependencies: [.product(name: "TelegramCore", package: "TelegramCore", condition: nil), .product(name: "Postbox", package: "Postbox", condition: nil), .product(name: "FFMpegBinding", package: "FFMpegBinding", condition: nil), diff --git a/submodules/PaymentMethodUI/Sources/PaymentCardEntryScreen.swift b/submodules/PaymentMethodUI/Sources/PaymentCardEntryScreen.swift index 79f2aa5dd88..01f2c454532 100644 --- a/submodules/PaymentMethodUI/Sources/PaymentCardEntryScreen.swift +++ b/submodules/PaymentMethodUI/Sources/PaymentCardEntryScreen.swift @@ -73,7 +73,7 @@ public final class ScrollComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ScrollComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ScrollComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let contentSize = self.contentView.update( transition: transition, component: component.content, @@ -96,7 +96,7 @@ public final class ScrollComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift index 3ea2d1aa772..9df6da5510e 100644 --- a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift @@ -1547,7 +1547,7 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { self.updateItems(size: size, transition: transition, stripTransition: transition) if let storyParams = self.storyParams { - var indicatorTransition = Transition(transition) + var indicatorTransition = ComponentTransition(transition) let expandedStorySetIndicator: ComponentView if let current = self.expandedStorySetIndicator { expandedStorySetIndicator = current @@ -1562,8 +1562,8 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { component: AnyComponent(StorySetIndicatorComponent( context: self.context, strings: self.context.sharedContext.currentPresentationData.with({ $0 }).strings, - peer: storyParams.peer, - items: storyParams.items, + items: storyParams.items.map { StorySetIndicatorComponent.Item(storyItem: $0, peer: storyParams.peer) }, + displayAvatars: false, hasUnseen: storyParams.hasUnseen, hasUnseenPrivate: storyParams.hasUnseenPrivate, totalCount: storyParams.count, diff --git a/submodules/PeerInfoUI/BUILD b/submodules/PeerInfoUI/BUILD index 600713ca150..417ff00e74f 100644 --- a/submodules/PeerInfoUI/BUILD +++ b/submodules/PeerInfoUI/BUILD @@ -77,6 +77,8 @@ swift_library( "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", "//submodules/TelegramUI/Components/SendInviteLinkScreen", "//submodules/TelegramUI/Components/GroupStickerPackSetupController", + "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", + "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", ], visibility = [ "//visibility:public", diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index 87ba5d676e5..914ed416acf 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -14,6 +14,8 @@ import Emoji import LocalizedPeerData import Markdown import SendInviteLinkScreen +import OwnershipTransferController +import OldChannelsController private let rankMaxLength: Int32 = 16 diff --git a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift index f7b2f1b24a7..bca367086ec 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift @@ -13,6 +13,7 @@ import AccountContext import AlertUI import PresentationDataUtils import ItemListAvatarAndNameInfoItem +import OldChannelsController private final class ChannelBannedMemberControllerArguments { let context: AccountContext diff --git a/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift b/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift index 0efb5f79603..3e283a4dd95 100644 --- a/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift @@ -15,6 +15,7 @@ import ItemListPeerItem import ItemListPeerActionItem import ChatListFilterSettingsHeaderItem import UndoUI +import OldChannelsController private final class ChannelDiscussionGroupSetupControllerArguments { let context: AccountContext diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index bdaeb73de9f..e26fd8e2871 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -18,6 +18,7 @@ import ItemListPeerActionItem import Markdown import UndoUI import Postbox +import OldChannelsController private final class ChannelPermissionsControllerArguments { let context: AccountContext diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index 87898ed2152..dd9e96b2f1b 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -3,6 +3,7 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit +import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences @@ -23,7 +24,8 @@ import UndoUI import QrCodeUI import PremiumUI import TextFormat -import Postbox +import PremiumUI +import OldChannelsController private final class ChannelVisibilityControllerArguments { let context: AccountContext @@ -2337,7 +2339,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta }) } else { if let navigationController = controller.navigationController as? NavigationController { - navigationController.replaceAllButRootController(context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default)), animated: true) + navigationController.replaceAllButRootController(context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil), animated: true) } } } diff --git a/submodules/PeerInfoUI/Sources/ConvertToSupergroupController.swift b/submodules/PeerInfoUI/Sources/ConvertToSupergroupController.swift index 5b13bed75d1..213f9cf10b4 100644 --- a/submodules/PeerInfoUI/Sources/ConvertToSupergroupController.swift +++ b/submodules/PeerInfoUI/Sources/ConvertToSupergroupController.swift @@ -149,7 +149,7 @@ public func convertToSupergroupController(context: AccountContext, peerId: Engin if !alreadyConverting { convertDisposable.set((context.engine.peers.convertGroupToSupergroup(peerId: peerId) |> deliverOnMainQueue).start(next: { createdPeerId in - replaceControllerImpl?(context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: createdPeerId), subject: nil, botStart: nil, mode: .standard(.default))) + replaceControllerImpl?(context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: createdPeerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)) })) } })]), nil) diff --git a/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift b/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift index 6b49dc70d3a..8bf2deb94ee 100644 --- a/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift +++ b/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift @@ -8,6 +8,7 @@ import TelegramPresentationData import ItemListUI import PresentationDataUtils import AccountContext +import OldChannelsController private final class GroupPreHistorySetupArguments { let toggle: (Bool) -> Void diff --git a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift index 0a87d44f1d1..182c4e2dc38 100644 --- a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift +++ b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift @@ -486,7 +486,7 @@ public func peersNearbyController(context: AccountContext) -> ViewController { })) }, contextAction: { peer, node, gesture in let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing)) + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) let contextController = ContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: peerNearbyContextMenuItems(context: context, peerId: peer.id, present: { c in presentControllerImpl?(c, nil) diff --git a/submodules/Postbox/Sources/ChatListViewState.swift b/submodules/Postbox/Sources/ChatListViewState.swift index b4fde57f499..ae901e7d202 100644 --- a/submodules/Postbox/Sources/ChatListViewState.swift +++ b/submodules/Postbox/Sources/ChatListViewState.swift @@ -1640,6 +1640,7 @@ struct ChatListViewState { includeFrom: true, to: .absoluteLowerBound().withPeerId(associatedMessageId.peerId).withNamespace(associatedMessageId.namespace), ignoreMessagesInTimestampRange: nil, + ignoreMessageIds: Set(), limit: 2 ) for innerMessage in innerMessages { diff --git a/submodules/Postbox/Sources/Message.swift b/submodules/Postbox/Sources/Message.swift index 255cf48937e..45b9aba3664 100644 --- a/submodules/Postbox/Sources/Message.swift +++ b/submodules/Postbox/Sources/Message.swift @@ -976,6 +976,10 @@ public final class StoreMessage { } } + public func withUpdatedMedia(_ media: [Media]) -> StoreMessage { + return StoreMessage(id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, threadId: self.threadId, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, authorId: self.authorId, text: self.text, attributes: self.attributes, media: media) + } + public func withUpdatedAttributes(_ attributes: [MessageAttribute]) -> StoreMessage { return StoreMessage(id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, threadId: self.threadId, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, authorId: self.authorId, text: self.text, attributes: attributes, media: self.media) } diff --git a/submodules/Postbox/Sources/MessageHistoryTable.swift b/submodules/Postbox/Sources/MessageHistoryTable.swift index 2b0bd53d314..f7631bd5f7a 100644 --- a/submodules/Postbox/Sources/MessageHistoryTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryTable.swift @@ -3233,7 +3233,7 @@ final class MessageHistoryTable: Table { return (messagesByMediaId, mediaRefs, count == 0 ? nil : lastIndex) } - func fetch(peerId: PeerId, namespace: MessageId.Namespace, tag: MessageTags?, customTag: MemoryBuffer?, threadId: Int64?, from fromIndex: MessageIndex, includeFrom: Bool, to toIndex: MessageIndex, ignoreMessagesInTimestampRange: ClosedRange?, limit: Int) -> [IntermediateMessage] { + func fetch(peerId: PeerId, namespace: MessageId.Namespace, tag: MessageTags?, customTag: MemoryBuffer?, threadId: Int64?, from fromIndex: MessageIndex, includeFrom: Bool, to toIndex: MessageIndex, ignoreMessagesInTimestampRange: ClosedRange?, ignoreMessageIds: Set, limit: Int) -> [IntermediateMessage] { precondition(fromIndex.id.peerId == toIndex.id.peerId) precondition(fromIndex.id.namespace == toIndex.id.namespace) @@ -3252,6 +3252,9 @@ final class MessageHistoryTable: Table { continue } } + if !ignoreMessageIds.isEmpty && ignoreMessageIds.contains(index.id) { + continue + } if fromIndex < toIndex { if index < fromIndex || index > toIndex { continue @@ -3280,6 +3283,9 @@ final class MessageHistoryTable: Table { continue } } + if !ignoreMessageIds.isEmpty && ignoreMessageIds.contains(index.id) { + continue + } if fromIndex < toIndex { if index < fromIndex || index > toIndex { continue @@ -3310,6 +3316,9 @@ final class MessageHistoryTable: Table { continue } } + if !ignoreMessageIds.isEmpty && ignoreMessageIds.contains(index.id) { + continue + } if fromIndex < toIndex { if index < fromIndex || index > toIndex { continue @@ -3349,6 +3358,9 @@ final class MessageHistoryTable: Table { continue } } + if !ignoreMessageIds.isEmpty && ignoreMessageIds.contains(index.id) { + continue + } if let tag = tag { if self.tagsTable.entryExists(tag: tag, index: index) { indices.append(index) @@ -3391,6 +3403,9 @@ final class MessageHistoryTable: Table { continue } } + if !ignoreMessageIds.isEmpty && ignoreMessageIds.contains(index.id) { + continue + } if fromIndex < toIndex { if index < fromIndex || index > toIndex { continue @@ -3406,7 +3421,7 @@ final class MessageHistoryTable: Table { assertionFailure() } } - } else if ignoreMessagesInTimestampRange != nil { + } else if ignoreMessagesInTimestampRange != nil || !ignoreMessageIds.isEmpty { var indices: [MessageIndex] = [] var startIndex = fromIndex var localIncludeFrom = includeFrom @@ -3441,6 +3456,9 @@ final class MessageHistoryTable: Table { continue } } + if !ignoreMessageIds.isEmpty && ignoreMessageIds.contains(index.id) { + continue + } indices.append(index) if indices.count >= limit { break diff --git a/submodules/Postbox/Sources/MessageHistoryView.swift b/submodules/Postbox/Sources/MessageHistoryView.swift index 9d09f85842e..2bd829b5c0d 100644 --- a/submodules/Postbox/Sources/MessageHistoryView.swift +++ b/submodules/Postbox/Sources/MessageHistoryView.swift @@ -305,6 +305,7 @@ public enum HistoryViewInputAnchor: Equatable { final class MutableMessageHistoryView: MutablePostboxView { private(set) var peerIds: MessageHistoryViewInput private let ignoreMessagesInTimestampRange: ClosedRange? + private let ignoreMessageIds: Set let tag: HistoryViewInputTag? private let appendMessagesFromTheSameGroup: Bool let namespaces: MessageIdNamespaces @@ -337,6 +338,7 @@ final class MutableMessageHistoryView: MutablePostboxView { trackHoles: Bool, peerIds: MessageHistoryViewInput, ignoreMessagesInTimestampRange: ClosedRange?, + ignoreMessageIds: Set, anchor inputAnchor: HistoryViewInputAnchor, combinedReadStates: MessageHistoryViewReadState?, transientReadStates: MessageHistoryViewReadState?, @@ -354,6 +356,7 @@ final class MutableMessageHistoryView: MutablePostboxView { self.trackHoles = trackHoles self.peerIds = peerIds self.ignoreMessagesInTimestampRange = ignoreMessagesInTimestampRange + self.ignoreMessageIds = ignoreMessageIds self.combinedReadStates = combinedReadStates self.transientReadStates = transientReadStates self.tag = tag @@ -382,12 +385,12 @@ final class MutableMessageHistoryView: MutablePostboxView { } } - self.state = HistoryViewState(postbox: postbox, inputAnchor: inputAnchor, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: self.orderStatistics, ignoreMessagesInTimestampRange: self.ignoreMessagesInTimestampRange, halfLimit: count + 1, locations: peerIds) + self.state = HistoryViewState(postbox: postbox, inputAnchor: inputAnchor, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: self.orderStatistics, ignoreMessagesInTimestampRange: self.ignoreMessagesInTimestampRange, ignoreMessageIds: self.ignoreMessageIds, halfLimit: count + 1, locations: peerIds) if case let .loading(loadingState) = self.state { let sampledState = loadingState.checkAndSample(postbox: postbox) switch sampledState { case let .ready(anchor, holes): - self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: tag, appendMessagesFromTheSameGroup: self.appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: self.orderStatistics, ignoreMessagesInTimestampRange: self.ignoreMessagesInTimestampRange, halfLimit: count + 1, locations: peerIds, postbox: postbox, holes: holes)) + self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: tag, appendMessagesFromTheSameGroup: self.appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: self.orderStatistics, ignoreMessagesInTimestampRange: self.ignoreMessagesInTimestampRange, ignoreMessageIds: self.ignoreMessageIds, halfLimit: count + 1, locations: peerIds, postbox: postbox, holes: holes)) self.sampledState = self.state.sample(postbox: postbox, clipHoles: self.clipHoles) case .loadHole: break @@ -401,12 +404,12 @@ final class MutableMessageHistoryView: MutablePostboxView { } private func reset(postbox: PostboxImpl) { - self.state = HistoryViewState(postbox: postbox, inputAnchor: self.anchor, tag: self.tag, appendMessagesFromTheSameGroup: self.appendMessagesFromTheSameGroup, namespaces: self.namespaces, statistics: self.orderStatistics, ignoreMessagesInTimestampRange: self.ignoreMessagesInTimestampRange, halfLimit: self.fillCount + 1, locations: self.peerIds) + self.state = HistoryViewState(postbox: postbox, inputAnchor: self.anchor, tag: self.tag, appendMessagesFromTheSameGroup: self.appendMessagesFromTheSameGroup, namespaces: self.namespaces, statistics: self.orderStatistics, ignoreMessagesInTimestampRange: self.ignoreMessagesInTimestampRange, ignoreMessageIds: self.ignoreMessageIds, halfLimit: self.fillCount + 1, locations: self.peerIds) if case let .loading(loadingState) = self.state { let sampledState = loadingState.checkAndSample(postbox: postbox) switch sampledState { case let .ready(anchor, holes): - self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, appendMessagesFromTheSameGroup: self.appendMessagesFromTheSameGroup, namespaces: self.namespaces, statistics: self.orderStatistics, ignoreMessagesInTimestampRange: self.ignoreMessagesInTimestampRange, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) + self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, appendMessagesFromTheSameGroup: self.appendMessagesFromTheSameGroup, namespaces: self.namespaces, statistics: self.orderStatistics, ignoreMessagesInTimestampRange: self.ignoreMessagesInTimestampRange, ignoreMessageIds: self.ignoreMessageIds, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) case .loadHole: break } @@ -415,7 +418,7 @@ final class MutableMessageHistoryView: MutablePostboxView { let sampledState = loadingState.checkAndSample(postbox: postbox) switch sampledState { case let .ready(anchor, holes): - self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, appendMessagesFromTheSameGroup: self.appendMessagesFromTheSameGroup, namespaces: self.namespaces, statistics: self.orderStatistics, ignoreMessagesInTimestampRange: self.ignoreMessagesInTimestampRange, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) + self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, appendMessagesFromTheSameGroup: self.appendMessagesFromTheSameGroup, namespaces: self.namespaces, statistics: self.orderStatistics, ignoreMessagesInTimestampRange: self.ignoreMessagesInTimestampRange, ignoreMessageIds: self.ignoreMessageIds, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) case .loadHole: break } @@ -771,7 +774,7 @@ final class MutableMessageHistoryView: MutablePostboxView { let sampledState = loadingState.checkAndSample(postbox: postbox) switch sampledState { case let .ready(anchor, holes): - self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, appendMessagesFromTheSameGroup: self.appendMessagesFromTheSameGroup, namespaces: self.namespaces, statistics: self.orderStatistics, ignoreMessagesInTimestampRange: self.ignoreMessagesInTimestampRange, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) + self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, appendMessagesFromTheSameGroup: self.appendMessagesFromTheSameGroup, namespaces: self.namespaces, statistics: self.orderStatistics, ignoreMessagesInTimestampRange: self.ignoreMessagesInTimestampRange, ignoreMessageIds: self.ignoreMessageIds, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) case .loadHole: break } diff --git a/submodules/Postbox/Sources/MessageHistoryViewState.swift b/submodules/Postbox/Sources/MessageHistoryViewState.swift index 04cb937f96b..c2812171f90 100644 --- a/submodules/Postbox/Sources/MessageHistoryViewState.swift +++ b/submodules/Postbox/Sources/MessageHistoryViewState.swift @@ -40,7 +40,7 @@ public enum MessageHistoryInput: Equatable, Hashable { } private extension MessageHistoryInput { - func fetch(postbox: PostboxImpl, peerId: PeerId, namespace: MessageId.Namespace, from fromIndex: MessageIndex, includeFrom: Bool, to toIndex: MessageIndex, ignoreMessagesInTimestampRange: ClosedRange?, limit: Int) -> [IntermediateMessage] { + func fetch(postbox: PostboxImpl, peerId: PeerId, namespace: MessageId.Namespace, from fromIndex: MessageIndex, includeFrom: Bool, to toIndex: MessageIndex, ignoreMessagesInTimestampRange: ClosedRange?, ignoreMessageIds: Set, limit: Int) -> [IntermediateMessage] { switch self { case let .automatic(threadId, tagInfo): var tag: MessageTags? @@ -65,7 +65,7 @@ private extension MessageHistoryInput { } } - var items = postbox.messageHistoryTable.fetch(peerId: peerId, namespace: namespace, tag: tag, customTag: customTag, threadId: threadId, from: fromIndex, includeFrom: includeFrom || shouldAddFromSameGroup, to: toIndex, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, limit: limit) + var items = postbox.messageHistoryTable.fetch(peerId: peerId, namespace: namespace, tag: tag, customTag: customTag, threadId: threadId, from: fromIndex, includeFrom: includeFrom || shouldAddFromSameGroup, to: toIndex, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, limit: limit) if shouldAddFromSameGroup { enum Direction { @@ -152,7 +152,7 @@ private extension MessageHistoryInput { case let .external(input, tag): switch input.content { case let .thread(peerId, id, _): - return postbox.messageHistoryTable.fetch(peerId: peerId, namespace: namespace, tag: tag, customTag: nil, threadId: id, from: fromIndex, includeFrom: includeFrom, to: toIndex, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, limit: limit) + return postbox.messageHistoryTable.fetch(peerId: peerId, namespace: namespace, tag: tag, customTag: nil, threadId: id, from: fromIndex, includeFrom: includeFrom, to: toIndex, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, limit: limit) case let .messages(allIndices, _, _): if allIndices.isEmpty { return [] @@ -213,6 +213,9 @@ private extension MessageHistoryInput { continue } } + if !ignoreMessageIds.isEmpty && ignoreMessageIds.contains(index.id) { + continue + } indices.append(index) } if indices.count >= limit { @@ -1301,18 +1304,20 @@ final class HistoryViewLoadedState { let input: MessageHistoryInput let statistics: MessageHistoryViewOrderStatistics let ignoreMessagesInTimestampRange: ClosedRange? + let ignoreMessageIds: Set let halfLimit: Int let seedConfiguration: SeedConfiguration var orderedEntriesBySpace: [PeerIdAndNamespace: OrderedHistoryViewEntries] var holes: HistoryViewHoles var spacesWithRemovals = Set() - init(anchor: HistoryViewAnchor, tag: HistoryViewInputTag?, appendMessagesFromTheSameGroup: Bool, namespaces: MessageIdNamespaces, statistics: MessageHistoryViewOrderStatistics, ignoreMessagesInTimestampRange: ClosedRange?, halfLimit: Int, locations: MessageHistoryViewInput, postbox: PostboxImpl, holes: HistoryViewHoles) { + init(anchor: HistoryViewAnchor, tag: HistoryViewInputTag?, appendMessagesFromTheSameGroup: Bool, namespaces: MessageIdNamespaces, statistics: MessageHistoryViewOrderStatistics, ignoreMessagesInTimestampRange: ClosedRange?, ignoreMessageIds: Set, halfLimit: Int, locations: MessageHistoryViewInput, postbox: PostboxImpl, holes: HistoryViewHoles) { precondition(halfLimit >= 3) self.anchor = anchor self.namespaces = namespaces self.statistics = statistics self.ignoreMessagesInTimestampRange = ignoreMessagesInTimestampRange + self.ignoreMessageIds = ignoreMessageIds self.halfLimit = halfLimit self.seedConfiguration = postbox.seedConfiguration self.orderedEntriesBySpace = [:] @@ -1419,7 +1424,7 @@ final class HistoryViewLoadedState { } else { nextLowerIndex = (anchorIndex, true) } - lowerOrAtAnchorMessages.append(contentsOf: self.input.fetch(postbox: postbox, peerId: space.peerId, namespace: space.namespace, from: nextLowerIndex.index, includeFrom: nextLowerIndex.includeFrom, to: lowerBound, ignoreMessagesInTimestampRange: self.ignoreMessagesInTimestampRange, limit: self.halfLimit - lowerOrAtAnchorMessages.count).map(mapEntry)) + lowerOrAtAnchorMessages.append(contentsOf: self.input.fetch(postbox: postbox, peerId: space.peerId, namespace: space.namespace, from: nextLowerIndex.index, includeFrom: nextLowerIndex.includeFrom, to: lowerBound, ignoreMessagesInTimestampRange: self.ignoreMessagesInTimestampRange, ignoreMessageIds: self.ignoreMessageIds, limit: self.halfLimit - lowerOrAtAnchorMessages.count).map(mapEntry)) } if higherThanAnchorMessages.count < self.halfLimit { let nextHigherIndex: MessageIndex @@ -1428,7 +1433,7 @@ final class HistoryViewLoadedState { } else { nextHigherIndex = anchorIndex } - higherThanAnchorMessages.append(contentsOf: self.input.fetch(postbox: postbox, peerId: space.peerId, namespace: space.namespace, from: nextHigherIndex, includeFrom: false, to: upperBound, ignoreMessagesInTimestampRange: self.ignoreMessagesInTimestampRange, limit: self.halfLimit - higherThanAnchorMessages.count).map(mapEntry)) + higherThanAnchorMessages.append(contentsOf: self.input.fetch(postbox: postbox, peerId: space.peerId, namespace: space.namespace, from: nextHigherIndex, includeFrom: false, to: upperBound, ignoreMessagesInTimestampRange: self.ignoreMessagesInTimestampRange, ignoreMessageIds: self.ignoreMessageIds, limit: self.halfLimit - higherThanAnchorMessages.count).map(mapEntry)) } lowerOrAtAnchorMessages.reverse() @@ -1703,6 +1708,9 @@ final class HistoryViewLoadedState { return false } } + if !self.ignoreMessageIds.isEmpty && self.ignoreMessageIds.contains(entry.index.id) { + return false + } let space = PeerIdAndNamespace(peerId: entry.index.id.peerId, namespace: entry.index.id.namespace) @@ -2120,90 +2128,90 @@ enum HistoryViewState { case loaded(HistoryViewLoadedState) case loading(HistoryViewLoadingState) - init(postbox: PostboxImpl, inputAnchor: HistoryViewInputAnchor, tag: HistoryViewInputTag?, appendMessagesFromTheSameGroup: Bool, namespaces: MessageIdNamespaces, statistics: MessageHistoryViewOrderStatistics, ignoreMessagesInTimestampRange: ClosedRange?, halfLimit: Int, locations: MessageHistoryViewInput) { + init(postbox: PostboxImpl, inputAnchor: HistoryViewInputAnchor, tag: HistoryViewInputTag?, appendMessagesFromTheSameGroup: Bool, namespaces: MessageIdNamespaces, statistics: MessageHistoryViewOrderStatistics, ignoreMessagesInTimestampRange: ClosedRange?, ignoreMessageIds: Set, halfLimit: Int, locations: MessageHistoryViewInput) { switch inputAnchor { - case let .index(index): - self = .loaded(HistoryViewLoadedState(anchor: .index(index), tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: statistics, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces)))) - case .lowerBound: - self = .loaded(HistoryViewLoadedState(anchor: .lowerBound, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: statistics, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces)))) - case .upperBound: - self = .loaded(HistoryViewLoadedState(anchor: .upperBound, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: statistics, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces)))) - case .unread: - let anchorPeerId: PeerId - switch locations { - case let .single(peerId, threadId): - anchorPeerId = peerId - if threadId != nil { - self = .loaded(HistoryViewLoadedState(anchor: .upperBound, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: statistics, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces)))) - return - } - case let .associated(peerId, _): - anchorPeerId = peerId - case .external: - self = .loaded(HistoryViewLoadedState(anchor: .upperBound, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: statistics, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces)))) - return + case let .index(index): + self = .loaded(HistoryViewLoadedState(anchor: .index(index), tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: statistics, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces)))) + case .lowerBound: + self = .loaded(HistoryViewLoadedState(anchor: .lowerBound, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: statistics, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces)))) + case .upperBound: + self = .loaded(HistoryViewLoadedState(anchor: .upperBound, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: statistics, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces)))) + case .unread: + let anchorPeerId: PeerId + switch locations { + case let .single(peerId, threadId): + anchorPeerId = peerId + if threadId != nil { + self = .loaded(HistoryViewLoadedState(anchor: .upperBound, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: statistics, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces)))) + return } - if postbox.chatListIndexTable.get(peerId: anchorPeerId).includedIndex(peerId: anchorPeerId) != nil, let combinedState = postbox.readStateTable.getCombinedState(anchorPeerId) { - var messageId: MessageId? - var anchor: HistoryViewAnchor? - loop: for (namespace, state) in combinedState.states { - switch state { - case let .idBased(maxIncomingReadId, _, _, count, _): - if count == 0 { - anchor = .upperBound - break loop - } else { - messageId = MessageId(peerId: anchorPeerId, namespace: namespace, id: maxIncomingReadId) - break loop - } - case let .indexBased(maxIncomingReadIndex, _, count, _): - if count == 0 { - anchor = .upperBound - break loop - } else { - anchor = .index(maxIncomingReadIndex) - break loop - } - } - } - if let messageId = messageId { - let loadingState = HistoryViewLoadingState(postbox: postbox, locations: locations, tag: tag, threadId: nil, namespaces: namespaces, messageId: messageId, halfLimit: halfLimit) - let sampledState = loadingState.checkAndSample(postbox: postbox) - switch sampledState { - case let .ready(anchor, holes): - self = .loaded(HistoryViewLoadedState(anchor: anchor, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: statistics, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: holes)) - case .loadHole: - self = .loading(loadingState) - } + case let .associated(peerId, _): + anchorPeerId = peerId + case .external: + self = .loaded(HistoryViewLoadedState(anchor: .upperBound, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: statistics, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces)))) + return + } + if postbox.chatListIndexTable.get(peerId: anchorPeerId).includedIndex(peerId: anchorPeerId) != nil, let combinedState = postbox.readStateTable.getCombinedState(anchorPeerId) { + var messageId: MessageId? + var anchor: HistoryViewAnchor? + loop: for (namespace, state) in combinedState.states { + switch state { + case let .idBased(maxIncomingReadId, _, _, count, _): + if count == 0 { + anchor = .upperBound + break loop } else { - self = .loaded(HistoryViewLoadedState(anchor: anchor ?? .upperBound, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: statistics, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces)))) + messageId = MessageId(peerId: anchorPeerId, namespace: namespace, id: maxIncomingReadId) + break loop } - } else { - preconditionFailure() - } - case let .message(messageId): - var threadId: Int64? - switch locations { - case let .single(_, threadIdValue): - threadId = threadIdValue - case let .external(input): - switch input.content { - case let .thread(_, id, _): - threadId = id - case .messages: - break + case let .indexBased(maxIncomingReadIndex, _, count, _): + if count == 0 { + anchor = .upperBound + break loop + } else { + anchor = .index(maxIncomingReadIndex) + break loop } - default: - break } - let loadingState = HistoryViewLoadingState(postbox: postbox, locations: locations, tag: tag, threadId: threadId, namespaces: namespaces, messageId: messageId, halfLimit: halfLimit) - let sampledState = loadingState.checkAndSample(postbox: postbox) - switch sampledState { + } + if let messageId = messageId { + let loadingState = HistoryViewLoadingState(postbox: postbox, locations: locations, tag: tag, threadId: nil, namespaces: namespaces, messageId: messageId, halfLimit: halfLimit) + let sampledState = loadingState.checkAndSample(postbox: postbox) + switch sampledState { case let .ready(anchor, holes): - self = .loaded(HistoryViewLoadedState(anchor: anchor, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: statistics, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: holes)) + self = .loaded(HistoryViewLoadedState(anchor: anchor, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: statistics, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: holes)) case .loadHole: self = .loading(loadingState) + } + } else { + self = .loaded(HistoryViewLoadedState(anchor: anchor ?? .upperBound, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: statistics, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces)))) + } + } else { + preconditionFailure() + } + case let .message(messageId): + var threadId: Int64? + switch locations { + case let .single(_, threadIdValue): + threadId = threadIdValue + case let .external(input): + switch input.content { + case let .thread(_, id, _): + threadId = id + case .messages: + break } + default: + break + } + let loadingState = HistoryViewLoadingState(postbox: postbox, locations: locations, tag: tag, threadId: threadId, namespaces: namespaces, messageId: messageId, halfLimit: halfLimit) + let sampledState = loadingState.checkAndSample(postbox: postbox) + switch sampledState { + case let .ready(anchor, holes): + self = .loaded(HistoryViewLoadedState(anchor: anchor, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, statistics: statistics, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: holes)) + case .loadHole: + self = .loading(loadingState) + } } } diff --git a/submodules/Postbox/Sources/MessageOfInterestHolesView.swift b/submodules/Postbox/Sources/MessageOfInterestHolesView.swift index 66c7377eecf..182b8fa7ff4 100644 --- a/submodules/Postbox/Sources/MessageOfInterestHolesView.swift +++ b/submodules/Postbox/Sources/MessageOfInterestHolesView.swift @@ -60,7 +60,7 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView { } } self.anchor = anchor - self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, trackHoles: true, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: []) + self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, trackHoles: true, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: []) let _ = self.updateFromView() } @@ -134,7 +134,7 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView { case let .peer(id, threadId): peerIds = postbox.peerIdsForLocation(.peer(peerId: id, threadId: threadId), ignoreRelatedChats: false) } - self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, trackHoles: false, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: []) + self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, trackHoles: false, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: []) return self.updateFromView() } else if self.wrappedView.replay(postbox: postbox, transaction: transaction) { var reloadView = false @@ -167,7 +167,7 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView { case let .peer(id, threadId): peerIds = postbox.peerIdsForLocation(.peer(peerId: id, threadId: threadId), ignoreRelatedChats: false) } - self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, trackHoles: false, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: []) + self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, trackHoles: false, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: []) } return self.updateFromView() diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 4d8313208d2..087184b05c4 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -1087,7 +1087,7 @@ public final class Transaction { guard let postbox = self.postbox else { return [] } - return postbox.messageHistoryTable.fetch(peerId: peerId, namespace: namespace, tag: nil, customTag: nil, threadId: threadId, from: from, includeFrom: includeFrom, to: to, ignoreMessagesInTimestampRange: nil, limit: limit).map(postbox.renderIntermediateMessage(_:)) + return postbox.messageHistoryTable.fetch(peerId: peerId, namespace: namespace, tag: nil, customTag: nil, threadId: threadId, from: from, includeFrom: includeFrom, to: to, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), limit: limit).map(postbox.renderIntermediateMessage(_:)) } public func getMessagesWithCustomTag(peerId: PeerId, namespace: MessageId.Namespace, threadId: Int64?, customTag: MemoryBuffer, from: MessageIndex, includeFrom: Bool, to: MessageIndex, limit: Int) -> [Message] { @@ -1095,7 +1095,7 @@ public final class Transaction { guard let postbox = self.postbox else { return [] } - return postbox.messageHistoryTable.fetch(peerId: peerId, namespace: namespace, tag: nil, customTag: customTag, threadId: threadId, from: from, includeFrom: includeFrom, to: to, ignoreMessagesInTimestampRange: nil, limit: limit).map(postbox.renderIntermediateMessage(_:)) + return postbox.messageHistoryTable.fetch(peerId: peerId, namespace: namespace, tag: nil, customTag: customTag, threadId: threadId, from: from, includeFrom: includeFrom, to: to, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), limit: limit).map(postbox.renderIntermediateMessage(_:)) } public func scanMessages(peerId: PeerId, namespace: MessageId.Namespace, tag: MessageTags, _ f: (Message) -> Bool) { @@ -1117,7 +1117,7 @@ public final class Transaction { self.postbox?.scanMessageAttributes(peerId: peerId, namespace: namespace, limit: limit, f) } - public func getMessagesHistoryViewState(input: MessageHistoryViewInput, ignoreMessagesInTimestampRange: ClosedRange?, count: Int, clipHoles: Bool, anchor: HistoryViewInputAnchor, namespaces: MessageIdNamespaces) -> MessageHistoryView { + public func getMessagesHistoryViewState(input: MessageHistoryViewInput, ignoreMessagesInTimestampRange: ClosedRange?, ignoreMessageIds: Set, count: Int, clipHoles: Bool, anchor: HistoryViewInputAnchor, namespaces: MessageIdNamespaces) -> MessageHistoryView { precondition(!self.disposed) guard let postbox = self.postbox else { preconditionFailure() @@ -1131,7 +1131,7 @@ public final class Transaction { view = next.0 }, error: { _ in }, completed: {}) - let disposable = postbox.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: input, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, clipHoles: clipHoles, anchor: anchor, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: namespaces, orderStatistics: MessageHistoryViewOrderStatistics(), additionalData: [], useRootInterfaceStateForThread: false) + let disposable = postbox.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: input, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, count: count, clipHoles: clipHoles, anchor: anchor, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: namespaces, orderStatistics: MessageHistoryViewOrderStatistics(), additionalData: [], useRootInterfaceStateForThread: false) disposable.dispose() return view! @@ -2294,7 +2294,7 @@ final class PostboxImpl { if let states = initialCombinedStates?.states { for (namespace, state) in states { if namespace != messageIndex.id.namespace && state.count != 0 { - if let item = self.messageHistoryTable.fetch(peerId: messageIndex.id.peerId, namespace: namespace, tag: nil, customTag: nil, threadId: nil, from: MessageIndex(id: MessageId(peerId: messageIndex.id.peerId, namespace: namespace, id: 1), timestamp: messageIndex.timestamp), includeFrom: true, to: MessageIndex.lowerBound(peerId: messageIndex.id.peerId, namespace: namespace), ignoreMessagesInTimestampRange: nil, limit: 1).first { + if let item = self.messageHistoryTable.fetch(peerId: messageIndex.id.peerId, namespace: namespace, tag: nil, customTag: nil, threadId: nil, from: MessageIndex(id: MessageId(peerId: messageIndex.id.peerId, namespace: namespace, id: 1), timestamp: messageIndex.timestamp), includeFrom: true, to: MessageIndex.lowerBound(peerId: messageIndex.id.peerId, namespace: namespace), ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), limit: 1).first { resultIds.append(contentsOf: self.messageHistoryTable.applyInteractiveMaxReadIndex(postbox: self, messageIndex: item.index, operationsByPeerId: &self.currentOperationsByPeerId, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations)) } } @@ -3148,7 +3148,7 @@ final class PostboxImpl { return peerIds } - public func aroundMessageOfInterestHistoryViewForChatLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange?, count: Int, clipHoles: Bool = true, topTaggedMessageIdNamespaces: Set, tag: HistoryViewInputTag?, appendMessagesFromTheSameGroup: Bool, namespaces: MessageIdNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, customUnreadMessageId: MessageId?, additionalData: [AdditionalMessageHistoryViewData], useRootInterfaceStateForThread: Bool) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + public func aroundMessageOfInterestHistoryViewForChatLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange?, ignoreMessageIds: Set, count: Int, clipHoles: Bool = true, topTaggedMessageIdNamespaces: Set, tag: HistoryViewInputTag?, appendMessagesFromTheSameGroup: Bool, namespaces: MessageIdNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, customUnreadMessageId: MessageId?, additionalData: [AdditionalMessageHistoryViewData], useRootInterfaceStateForThread: Bool) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { return self.resolvedChatLocationInput(chatLocation: chatLocation) |> mapToSignal { chatLocationData in let (chatLocation, isHoleFill) = chatLocationData @@ -3206,7 +3206,7 @@ final class PostboxImpl { anchor = .upperBound } } - return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, clipHoles: clipHoles, anchor: anchor, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, orderStatistics: orderStatistics, additionalData: additionalData, useRootInterfaceStateForThread: useRootInterfaceStateForThread) + return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, count: count, clipHoles: clipHoles, anchor: anchor, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, orderStatistics: orderStatistics, additionalData: additionalData, useRootInterfaceStateForThread: useRootInterfaceStateForThread) }) return signal @@ -3220,13 +3220,13 @@ final class PostboxImpl { } } - public func aroundIdMessageHistoryViewForLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange?, count: Int, clipHoles: Bool = true, ignoreRelatedChats: Bool = false, messageId: MessageId, topTaggedMessageIdNamespaces: Set, tag: HistoryViewInputTag?, appendMessagesFromTheSameGroup: Bool, namespaces: MessageIdNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData] = [], useRootInterfaceStateForThread: Bool = false) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + public func aroundIdMessageHistoryViewForLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange?, ignoreMessageIds: Set, count: Int, clipHoles: Bool = true, ignoreRelatedChats: Bool = false, messageId: MessageId, topTaggedMessageIdNamespaces: Set, tag: HistoryViewInputTag?, appendMessagesFromTheSameGroup: Bool, namespaces: MessageIdNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData] = [], useRootInterfaceStateForThread: Bool = false) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { return self.resolvedChatLocationInput(chatLocation: chatLocation) |> mapToSignal { chatLocationData in let (chatLocation, isHoleFill) = chatLocationData let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> = self.transactionSignal { subscriber, transaction in let peerIds = self.peerIdsForLocation(chatLocation, ignoreRelatedChats: ignoreRelatedChats) - return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, clipHoles: clipHoles, anchor: .message(messageId), fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, orderStatistics: orderStatistics, additionalData: additionalData, useRootInterfaceStateForThread: useRootInterfaceStateForThread) + return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, count: count, clipHoles: clipHoles, anchor: .message(messageId), fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, orderStatistics: orderStatistics, additionalData: additionalData, useRootInterfaceStateForThread: useRootInterfaceStateForThread) } return signal @@ -3240,14 +3240,14 @@ final class PostboxImpl { } } - public func aroundMessageHistoryViewForLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange?, anchor: HistoryViewInputAnchor, count: Int, clipHoles: Bool = true, ignoreRelatedChats: Bool = false, fixedCombinedReadStates: MessageHistoryViewReadState?, topTaggedMessageIdNamespaces: Set, tag: HistoryViewInputTag?, appendMessagesFromTheSameGroup: Bool, namespaces: MessageIdNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData] = [], useRootInterfaceStateForThread: Bool = false) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + public func aroundMessageHistoryViewForLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange?, ignoreMessageIds: Set, anchor: HistoryViewInputAnchor, count: Int, clipHoles: Bool = true, ignoreRelatedChats: Bool = false, fixedCombinedReadStates: MessageHistoryViewReadState?, topTaggedMessageIdNamespaces: Set, tag: HistoryViewInputTag?, appendMessagesFromTheSameGroup: Bool, namespaces: MessageIdNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData] = [], useRootInterfaceStateForThread: Bool = false) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { return self.resolvedChatLocationInput(chatLocation: chatLocation) |> mapToSignal { chatLocationData -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> in let (chatLocation, isHoleFill) = chatLocationData let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> = self.transactionSignal { subscriber, transaction in let peerIds = self.peerIdsForLocation(chatLocation, ignoreRelatedChats: ignoreRelatedChats) - return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, clipHoles: clipHoles, anchor: anchor, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, orderStatistics: orderStatistics, additionalData: additionalData, useRootInterfaceStateForThread: useRootInterfaceStateForThread) + return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, count: count, clipHoles: clipHoles, anchor: anchor, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, orderStatistics: orderStatistics, additionalData: additionalData, useRootInterfaceStateForThread: useRootInterfaceStateForThread) } return signal @@ -3266,6 +3266,7 @@ final class PostboxImpl { subscriber: Subscriber<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError>, peerIds: MessageHistoryViewInput, ignoreMessagesInTimestampRange: ClosedRange?, + ignoreMessageIds: Set, count: Int, clipHoles: Bool, anchor: HistoryViewInputAnchor, @@ -3375,7 +3376,7 @@ final class PostboxImpl { readStates = transientReadStates } - let mutableView = MutableMessageHistoryView(postbox: self, orderStatistics: orderStatistics, clipHoles: clipHoles, trackHoles: true, peerIds: peerIds, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, anchor: anchor, combinedReadStates: readStates, transientReadStates: transientReadStates, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, count: count, topTaggedMessages: topTaggedMessages, additionalDatas: additionalDataEntries) + let mutableView = MutableMessageHistoryView(postbox: self, orderStatistics: orderStatistics, clipHoles: clipHoles, trackHoles: true, peerIds: peerIds, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, anchor: anchor, combinedReadStates: readStates, transientReadStates: transientReadStates, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, count: count, topTaggedMessages: topTaggedMessages, additionalDatas: additionalDataEntries) let initialUpdateType: ViewUpdateType = .Initial @@ -4116,7 +4117,7 @@ final class PostboxImpl { var index = MessageIndex.upperBound(peerId: peerId, namespace: namespace) var remainingLimit = limit while remainingLimit > 0 { - let messages = self.messageHistoryTable.fetch(peerId: peerId, namespace: namespace, tag: nil, customTag: nil, threadId: nil, from: index, includeFrom: false, to: lowerBound, ignoreMessagesInTimestampRange: nil, limit: 10) + let messages = self.messageHistoryTable.fetch(peerId: peerId, namespace: namespace, tag: nil, customTag: nil, threadId: nil, from: index, includeFrom: false, to: lowerBound, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), limit: 10) remainingLimit -= 10 for message in messages { if !f(self.renderIntermediateMessage(message)) { @@ -4135,7 +4136,7 @@ final class PostboxImpl { var remainingLimit = limit var index = MessageIndex.upperBound(peerId: peerId, namespace: namespace) while remainingLimit > 0 { - let messages = self.messageHistoryTable.fetch(peerId: peerId, namespace: namespace, tag: nil, customTag: nil, threadId: nil, from: index, includeFrom: false, to: MessageIndex.lowerBound(peerId: peerId, namespace: namespace), ignoreMessagesInTimestampRange: nil, limit: 32) + let messages = self.messageHistoryTable.fetch(peerId: peerId, namespace: namespace, tag: nil, customTag: nil, threadId: nil, from: index, includeFrom: false, to: MessageIndex.lowerBound(peerId: peerId, namespace: namespace), ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), limit: 32) for message in messages { let attributes = MessageHistoryTable.renderMessageAttributes(message) if !f(message.id, attributes) { @@ -4447,6 +4448,7 @@ public class Postbox { public func aroundMessageOfInterestHistoryViewForChatLocation( _ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange?, + ignoreMessageIds: Set, count: Int, clipHoles: Bool = true, topTaggedMessageIdNamespaces: Set, @@ -4465,6 +4467,7 @@ public class Postbox { disposable.set(impl.aroundMessageOfInterestHistoryViewForChatLocation( chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, + ignoreMessageIds: ignoreMessageIds, count: count, clipHoles: clipHoles, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, @@ -4485,6 +4488,7 @@ public class Postbox { public func aroundIdMessageHistoryViewForLocation( _ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange?, + ignoreMessageIds: Set, count: Int, clipHoles: Bool = true, ignoreRelatedChats: Bool = false, @@ -4504,6 +4508,7 @@ public class Postbox { disposable.set(impl.aroundIdMessageHistoryViewForLocation( chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, + ignoreMessageIds: ignoreMessageIds, count: count, clipHoles: clipHoles, ignoreRelatedChats: ignoreRelatedChats, @@ -4526,6 +4531,7 @@ public class Postbox { _ chatLocation: ChatLocationInput, anchor: HistoryViewInputAnchor, ignoreMessagesInTimestampRange: ClosedRange?, + ignoreMessageIds: Set, count: Int, clipHoles: Bool = true, ignoreRelatedChats: Bool = false, @@ -4545,6 +4551,7 @@ public class Postbox { disposable.set(impl.aroundMessageHistoryViewForLocation( chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, + ignoreMessageIds: ignoreMessageIds, anchor: anchor, count: count, clipHoles: clipHoles, diff --git a/submodules/Postbox/Sources/Views.swift b/submodules/Postbox/Sources/Views.swift index 48dd9e5633e..3570dbbf693 100644 --- a/submodules/Postbox/Sources/Views.swift +++ b/submodules/Postbox/Sources/Views.swift @@ -8,6 +8,7 @@ public enum PostboxViewKey: Hashable { public var trackHoles: Bool public var orderStatistics: MessageHistoryViewOrderStatistics public var ignoreMessagesInTimestampRange: ClosedRange? + public var ignoreMessageIds: Set public var anchor: HistoryViewInputAnchor public var combinedReadStates: MessageHistoryViewReadState? public var transientReadStates: MessageHistoryViewReadState? @@ -23,6 +24,7 @@ public enum PostboxViewKey: Hashable { trackHoles: Bool, orderStatistics: MessageHistoryViewOrderStatistics = [], ignoreMessagesInTimestampRange: ClosedRange? = nil, + ignoreMessageIds: Set = Set(), anchor: HistoryViewInputAnchor, combinedReadStates: MessageHistoryViewReadState? = nil, transientReadStates: MessageHistoryViewReadState? = nil, @@ -37,6 +39,7 @@ public enum PostboxViewKey: Hashable { self.trackHoles = trackHoles self.orderStatistics = orderStatistics self.ignoreMessagesInTimestampRange = ignoreMessagesInTimestampRange + self.ignoreMessageIds = ignoreMessageIds self.anchor = anchor self.combinedReadStates = combinedReadStates self.transientReadStates = transientReadStates @@ -647,6 +650,7 @@ func postboxViewForKey(postbox: PostboxImpl, key: PostboxViewKey) -> MutablePost trackHoles: historyView.trackHoles, peerIds: .single(peerId: historyView.peerId, threadId: historyView.threadId), ignoreMessagesInTimestampRange: historyView.ignoreMessagesInTimestampRange, + ignoreMessageIds: historyView.ignoreMessageIds, anchor: historyView.anchor, combinedReadStates: historyView.combinedReadStates, transientReadStates: historyView.transientReadStates, diff --git a/submodules/PremiumUI/Sources/AppIconsDemoComponent.swift b/submodules/PremiumUI/Sources/AppIconsDemoComponent.swift index 7c0fb637989..ba86b822453 100644 --- a/submodules/PremiumUI/Sources/AppIconsDemoComponent.swift +++ b/submodules/PremiumUI/Sources/AppIconsDemoComponent.swift @@ -55,7 +55,7 @@ final class AppIconsDemoComponent: Component { fatalError("init(coder:) has not been implemented") } - public func update(component: AppIconsDemoComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + public func update(component: AppIconsDemoComponent, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { let isDisplaying = environment[DemoPageEnvironment.self].isDisplaying self.component = component @@ -173,7 +173,7 @@ final class AppIconsDemoComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } diff --git a/submodules/PremiumUI/Sources/BadgeLabelView.swift b/submodules/PremiumUI/Sources/BadgeLabelView.swift index 3d7be34fdb0..6e24d18396c 100644 --- a/submodules/PremiumUI/Sources/BadgeLabelView.swift +++ b/submodules/PremiumUI/Sources/BadgeLabelView.swift @@ -48,7 +48,7 @@ final class BadgeLabelView: UIView { fatalError("init(coder:) has not been implemented") } - func update(value: Int32, isFirst: Bool, isLast: Bool, transition: Transition) { + func update(value: Int32, isFirst: Bool, isLast: Bool, transition: ComponentTransition) { let previousValue = self.currentValue self.currentValue = value @@ -98,7 +98,7 @@ final class BadgeLabelView: UIView { } } - func update(value: String, transition: Transition) -> CGSize { + func update(value: String, transition: ComponentTransition) -> CGSize { if value.contains(" ") { for (_, view) in self.itemViews { view.isHidden = true diff --git a/submodules/PremiumUI/Sources/BoostHeaderBackgroundComponent.swift b/submodules/PremiumUI/Sources/BoostHeaderBackgroundComponent.swift index 566228e5e45..82ebab8dcc1 100644 --- a/submodules/PremiumUI/Sources/BoostHeaderBackgroundComponent.swift +++ b/submodules/PremiumUI/Sources/BoostHeaderBackgroundComponent.swift @@ -80,7 +80,7 @@ public final class BoostHeaderBackgroundComponent: Component { } } - func update(component: BoostHeaderBackgroundComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: BoostHeaderBackgroundComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height)) if self.sceneView.superview == self { self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0) @@ -96,7 +96,7 @@ public final class BoostHeaderBackgroundComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/PremiumUI/Sources/BusinessPageComponent.swift b/submodules/PremiumUI/Sources/BusinessPageComponent.swift index fba7895323f..c89b4a0a08d 100644 --- a/submodules/PremiumUI/Sources/BusinessPageComponent.swift +++ b/submodules/PremiumUI/Sources/BusinessPageComponent.swift @@ -48,7 +48,7 @@ private final class HeaderComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: HeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: HeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -95,7 +95,7 @@ private final class HeaderComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/PremiumUI/Sources/CreateGiveawayFooterItem.swift b/submodules/PremiumUI/Sources/CreateGiveawayFooterItem.swift index 291d4d357da..066dfb35365 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayFooterItem.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayFooterItem.swift @@ -103,7 +103,7 @@ final class CreateGiveawayFooterItemNode: ItemListControllerFooterItemNode { let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - totalPanelHeight), size: CGSize(width: layout.size.width, height: panelHeight)) - var buttonTransition: Transition = .easeInOut(duration: 0.2) + var buttonTransition: ComponentTransition = .easeInOut(duration: 0.2) if !hadLayout { buttonTransition = .immediate } diff --git a/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift b/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift index 765be52fb3f..80b3dbd10fe 100644 --- a/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift +++ b/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift @@ -136,7 +136,7 @@ class EmojiHeaderComponent: Component { self.containerView = nil } - func update(component: EmojiHeaderComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: EmojiHeaderComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.hasIdleAnimations = component.hasIdleAnimations let size = self.statusView.update( @@ -168,7 +168,7 @@ class EmojiHeaderComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/PeerInfoUI/Sources/IncreaseLimitFooterItem.swift b/submodules/PremiumUI/Sources/IncreaseLimitFooterItem.swift similarity index 94% rename from submodules/PeerInfoUI/Sources/IncreaseLimitFooterItem.swift rename to submodules/PremiumUI/Sources/IncreaseLimitFooterItem.swift index befb39d4f9a..3b9493ef116 100644 --- a/submodules/PeerInfoUI/Sources/IncreaseLimitFooterItem.swift +++ b/submodules/PremiumUI/Sources/IncreaseLimitFooterItem.swift @@ -8,20 +8,20 @@ import PresentationDataUtils import SolidRoundedButtonNode import AppBundle -final class IncreaseLimitFooterItem: ItemListControllerFooterItem { +public final class IncreaseLimitFooterItem: ItemListControllerFooterItem { let theme: PresentationTheme let title: String let colorful: Bool let action: () -> Void - init(theme: PresentationTheme, title: String, colorful: Bool, action: @escaping () -> Void) { + public init(theme: PresentationTheme, title: String, colorful: Bool, action: @escaping () -> Void) { self.theme = theme self.title = title self.colorful = colorful self.action = action } - func isEqual(to: ItemListControllerFooterItem) -> Bool { + public func isEqual(to: ItemListControllerFooterItem) -> Bool { if let item = to as? IncreaseLimitFooterItem { return self.theme === item.theme && self.title == item.title && self.colorful == item.colorful } else { @@ -29,7 +29,7 @@ final class IncreaseLimitFooterItem: ItemListControllerFooterItem { } } - func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode { + public func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode { if let current = current as? IncreaseLimitFooterItemNode { current.item = self return current diff --git a/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift b/submodules/PremiumUI/Sources/IncreaseLimitHeaderItem.swift similarity index 92% rename from submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift rename to submodules/PremiumUI/Sources/IncreaseLimitHeaderItem.swift index f9410539382..0637c7fb803 100644 --- a/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift +++ b/submodules/PremiumUI/Sources/IncreaseLimitHeaderItem.swift @@ -7,11 +7,10 @@ import TelegramPresentationData import ItemListUI import PresentationDataUtils import Markdown -import PremiumUI import ComponentFlow -class IncreaseLimitHeaderItem: ListViewItem, ItemListItem { - enum Icon { +public class IncreaseLimitHeaderItem: ListViewItem, ItemListItem { + public enum Icon { case group case link } @@ -27,10 +26,10 @@ class IncreaseLimitHeaderItem: ListViewItem, ItemListItem { let premiumCount: Int32 let text: String let isPremiumDisabled: Bool - let sectionId: ItemListSectionId + public let sectionId: ItemListSectionId // MARK: Nicegram JoinGroupLimit, nicegramNotice added - init(nicegramNotice: String? = nil, theme: PresentationTheme, strings: PresentationStrings, icon: Icon, count: Int32, limit: Int32, premiumCount: Int32, text: String, isPremiumDisabled: Bool, sectionId: ItemListSectionId) { + public init(nicegramNotice: String? = nil, theme: PresentationTheme, strings: PresentationStrings, icon: Icon, count: Int32, limit: Int32, premiumCount: Int32, text: String, isPremiumDisabled: Bool, sectionId: ItemListSectionId) { // MARK: Nicegram JoinGroupLimit self.nicegramNotice = nicegramNotice // @@ -45,7 +44,7 @@ class IncreaseLimitHeaderItem: ListViewItem, ItemListItem { self.sectionId = sectionId } - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = IncreaseLimitHeaderItemNode() let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) @@ -61,7 +60,7 @@ class IncreaseLimitHeaderItem: ListViewItem, ItemListItem { } } - 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) { + 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 { guard let nodeValue = node() as? IncreaseLimitHeaderItemNode else { assertionFailure() @@ -263,7 +262,7 @@ class IncreaseLimitHeaderItemNode: ListViewItemNode { badgeIconName: badgeIconName, badgeText: "\(item.count)", badgePosition: CGFloat(item.count) / CGFloat(item.premiumCount), - badgeGraphPosition: CGFloat(item.count) / CGFloat(item.premiumCount), + badgeGraphPosition: CGFloat(item.limit) / CGFloat(item.premiumCount), isPremiumDisabled: item.isPremiumDisabled )) let containerSize = CGSize(width: layout.size.width - params.leftInset - params.rightInset, height: 200.0) diff --git a/submodules/PremiumUI/Sources/PageIndicatorComponent.swift b/submodules/PremiumUI/Sources/PageIndicatorComponent.swift index 1e5497a5292..34ea6a940bb 100644 --- a/submodules/PremiumUI/Sources/PageIndicatorComponent.swift +++ b/submodules/PremiumUI/Sources/PageIndicatorComponent.swift @@ -54,7 +54,7 @@ public final class PageIndicatorComponent: Component { fatalError("init(coder:) has not been implemented") } - public func update(component: PageIndicatorComponent, availableSize: CGSize, transition: Transition) -> CGSize { + public func update(component: PageIndicatorComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let isFirstTime = self.component == nil self.component = component @@ -75,7 +75,7 @@ public final class PageIndicatorComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift index d65a427ff33..34b2ff7a8e1 100644 --- a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift +++ b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift @@ -475,7 +475,7 @@ final class PhoneDemoComponent: Component { self.playbackStatusDisposable?.dispose() } - public func update(component: PhoneDemoComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + public func update(component: PhoneDemoComponent, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.containerView.frame = CGRect(origin: .zero, size: availableSize) @@ -614,7 +614,7 @@ final class PhoneDemoComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } diff --git a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift index f41ee7c9747..b1a7eb09634 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift @@ -861,11 +861,11 @@ private final class SheetContent: CombinedComponent { ) context.add(alternateText .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + alternateText.size.height / 2.0)) - .appear(Transition.Appear({ _, view, transition in + .appear(ComponentTransition.Appear({ _, view, transition in transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: 64.0), to: .zero, additive: true) transition.animateAlpha(view: view, from: 0.0, to: 1.0) })) - .disappear(Transition.Disappear({ view, transition, completion in + .disappear(ComponentTransition.Disappear({ view, transition, completion in view.superview?.sendSubviewToBack(view) transition.animatePosition(view: view, from: .zero, to: CGPoint(x: 0.0, y: -64.0), additive: true) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in @@ -887,11 +887,11 @@ private final class SheetContent: CombinedComponent { ) context.add(text .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0)) - .appear(Transition.Appear({ _, view, transition in + .appear(ComponentTransition.Appear({ _, view, transition in transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: 64.0), to: .zero, additive: true) transition.animateAlpha(view: view, from: 0.0, to: 1.0) })) - .disappear(Transition.Disappear({ view, transition, completion in + .disappear(ComponentTransition.Disappear({ view, transition, completion in view.superview?.sendSubviewToBack(view) transition.animatePosition(view: view, from: .zero, to: CGPoint(x: 0.0, y: -64.0), additive: true) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in @@ -1792,7 +1792,7 @@ public class PremiumBoostLevelsScreen: ViewController { self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) } - func requestLayout(transition: Transition) { + func requestLayout(transition: ComponentTransition) { guard let layout = self.currentLayout else { return } @@ -1800,7 +1800,7 @@ public class PremiumBoostLevelsScreen: ViewController { } private var dismissOffset: CGFloat? - func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, transition: Transition) { + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, transition: ComponentTransition) { guard !self.isDismissing else { return } @@ -1897,7 +1897,7 @@ public class PremiumBoostLevelsScreen: ViewController { } private var boostState: InternalBoostState.DisplayData? - func updated(transition: Transition, forceUpdate: Bool = false) { + func updated(transition: ComponentTransition, forceUpdate: Bool = false) { guard let controller = self.controller else { return } @@ -2165,24 +2165,24 @@ public class PremiumBoostLevelsScreen: ViewController { let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) - self.containerLayoutUpdated(layout: layout, transition: Transition(transition)) + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) } else { self.isExpanded = true - self.containerLayoutUpdated(layout: layout, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) } } else if scrollView != nil, (velocity.y < -300.0 || offset < topInset / 2.0) { let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset) let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) self.isExpanded = true - self.containerLayoutUpdated(layout: layout, transition: Transition(transition)) + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) } else { if let scrollView = scrollView { scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) } - self.containerLayoutUpdated(layout: layout, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) } if !dismissing { @@ -2195,7 +2195,7 @@ public class PremiumBoostLevelsScreen: ViewController { case .cancelled: self.panGestureArguments = nil - self.containerLayoutUpdated(layout: layout, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) default: break } @@ -2220,7 +2220,7 @@ public class PremiumBoostLevelsScreen: ViewController { guard let layout = self.currentLayout else { return } - self.containerLayoutUpdated(layout: layout, transition: Transition(transition)) + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) } private var currentMyBoostCount: Int32 = 0 @@ -2560,7 +2560,7 @@ public class PremiumBoostLevelsScreen: ViewController { self.currentLayout = layout super.containerLayoutUpdated(layout, transition: transition) - self.node.containerLayoutUpdated(layout: layout, transition: Transition(transition)) + self.node.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) } } @@ -2614,7 +2614,7 @@ private final class FooterComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: FooterComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: FooterComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -2677,7 +2677,7 @@ private final class FooterComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/PremiumUI/Sources/PremiumCoinComponent.swift b/submodules/PremiumUI/Sources/PremiumCoinComponent.swift index 84a156e022e..4d8c4e0bb38 100644 --- a/submodules/PremiumUI/Sources/PremiumCoinComponent.swift +++ b/submodules/PremiumUI/Sources/PremiumCoinComponent.swift @@ -495,7 +495,7 @@ class PremiumCoinComponent: Component { node.addAnimation(springAnimation, forKey: "rotate") } - func update(component: PremiumCoinComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: PremiumCoinComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0)) if self.sceneView.superview == self { self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0) @@ -511,7 +511,7 @@ class PremiumCoinComponent: Component { return View(frame: CGRect(), isIntro: self.isIntro) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 91671972a03..fc876e6e819 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -69,7 +69,7 @@ public final class PremiumGradientBackgroundComponent: Component { } - func update(component: PremiumGradientBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PremiumGradientBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.clipLayer.frame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: availableSize.height + 10.0)) self.gradientLayer.frame = CGRect(origin: .zero, size: availableSize) @@ -149,7 +149,7 @@ public final class PremiumGradientBackgroundComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -371,7 +371,7 @@ final class DemoPagerComponent: Component { self.ignoreContentOffsetChange = false } - func update(component: DemoPagerComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: DemoPagerComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { var validIds: [AnyHashable] = [] component.nextAction?.connect { [weak self] in @@ -473,7 +473,7 @@ final class DemoPagerComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -666,7 +666,7 @@ private final class DemoSheetContent: CombinedComponent { strongSelf.isPremium = isPremium strongSelf.promoConfiguration = promoConfiguration if !reactions.isEmpty && !stickers.isEmpty { - strongSelf.updated(transition: Transition(.immediate).withUserData(DemoAnimateInTransition())) + strongSelf.updated(transition: ComponentTransition(.immediate).withUserData(DemoAnimateInTransition())) } }) } @@ -1078,6 +1078,26 @@ private final class DemoSheetContent: CombinedComponent { ) ) + availableItems[.messageEffects] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.messageEffects, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: component.context, + position: .top, + model: .island, + videoFile: configuration.videos["effects"], + decoration: .swirlStars + )), + title: strings.Premium_MessageEffects, + text: strings.Premium_MessageEffectsInfo, + textColor: textColor + ) + ) + ) + ) + let index: Int = 0 var items: [DemoPagerComponent.Item] = [] if let item = availableItems.first(where: { $0.value.content.id == component.subject as AnyHashable }) { @@ -1172,6 +1192,8 @@ private final class DemoSheetContent: CombinedComponent { text = strings.Premium_MessagePrivacyInfo case .folderTags: text = strings.Premium_FolderTagsStandaloneInfo + case .messageEffects: + text = strings.Premium_MessageEffectsInfo default: text = "" } @@ -1441,6 +1463,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer { case messagePrivacy case business case folderTags + case messageEffects case businessLocation case businessHours @@ -1497,6 +1520,8 @@ public class PremiumDemoScreen: ViewControllerComponentContainer { return .business case .folderTags: return .folderTags + case .messageEffects: + return .messageEffects case .businessLocation: return .businessLocation case .businessHours: diff --git a/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift index 8869e2d91da..33aad007493 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift @@ -1102,7 +1102,7 @@ private final class PeerCellComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: PeerCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PeerCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -1148,7 +1148,7 @@ private final class PeerCellComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -1183,7 +1183,7 @@ private final class DustComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: DustComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: DustComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -1198,7 +1198,7 @@ private final class DustComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index 8e3668042e7..900a4d0cd66 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -426,18 +426,19 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { UIColor(rgb: 0xef6922), UIColor(rgb: 0xe95a2c), UIColor(rgb: 0xe74e33), - UIColor(rgb: 0xe74e33), //replace + UIColor(rgb: 0xe74e33), UIColor(rgb: 0xe54937), UIColor(rgb: 0xe3433c), UIColor(rgb: 0xdb374b), UIColor(rgb: 0xcb3e6d), UIColor(rgb: 0xbc4395), UIColor(rgb: 0xab4ac4), + UIColor(rgb: 0xab4ac4), UIColor(rgb: 0xa34cd7), UIColor(rgb: 0x9b4fed), UIColor(rgb: 0x8958ff), UIColor(rgb: 0x676bff), - UIColor(rgb: 0x676bff), //replace + UIColor(rgb: 0x676bff), UIColor(rgb: 0x6172ff), UIColor(rgb: 0x5b79ff), UIColor(rgb: 0x4492ff), @@ -534,6 +535,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { demoSubject = .lastSeen case .messagePrivacy: demoSubject = .messagePrivacy + case .messageEffects: + demoSubject = .messageEffects case .business: demoSubject = .business default: diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 38f3578b57c..c43a7ea6016 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -246,6 +246,12 @@ public enum PremiumSource: Equatable { } else { return false } + case .storiesLinks: + if case .storiesLinks = rhs { + return true + } else { + return false + } case let .channelBoost(peerId): if case .channelBoost(peerId) = rhs { return true @@ -294,6 +300,12 @@ public enum PremiumSource: Equatable { } else { return false } + case .messageEffects: + if case .messageEffects = rhs { + return true + } else { + return false + } } } @@ -330,6 +342,7 @@ public enum PremiumSource: Equatable { case storiesFormatting case storiesExpirationDurations case storiesSuggestedReactions + case storiesLinks case storiesHigherQuality case channelBoost(EnginePeer.Id) case nameColor @@ -339,6 +352,7 @@ public enum PremiumSource: Equatable { case readTime case messageTags case folderTags + case messageEffects var identifier: String? { switch self { @@ -410,6 +424,8 @@ public enum PremiumSource: Equatable { return "stories__expiration_durations" case .storiesSuggestedReactions: return "stories__suggested_reactions" + case .storiesLinks: + return "stories__links" case .storiesHigherQuality: return "stories__quality" case let .channelBoost(peerId): @@ -428,6 +444,8 @@ public enum PremiumSource: Equatable { return "saved_tags" case .folderTags: return "folder_tags" + case .messageEffects: + return "effects" } } } @@ -455,6 +473,7 @@ public enum PremiumPerk: CaseIterable { case messagePrivacy case business case folderTags + case messageEffects case businessLocation case businessHours @@ -488,7 +507,8 @@ public enum PremiumPerk: CaseIterable { .lastSeen, .messagePrivacy, .folderTags, - .business + .business, + .messageEffects ] } @@ -560,6 +580,8 @@ public enum PremiumPerk: CaseIterable { return "message_privacy" case .folderTags: return "folder_tags" + case .messageEffects: + return "effects" case .business: return "business" case .businessLocation: @@ -627,7 +649,8 @@ public enum PremiumPerk: CaseIterable { return strings.Premium_FolderTags case .business: return strings.Premium_Business - + case .messageEffects: + return strings.Premium_MessageEffects case .businessLocation: return strings.Business_Location case .businessHours: @@ -693,7 +716,8 @@ public enum PremiumPerk: CaseIterable { return strings.Premium_FolderTagsInfo case .business: return strings.Premium_BusinessInfo - + case .messageEffects: + return strings.Premium_MessageEffectsInfo case .businessLocation: return strings.Business_LocationInfo case .businessHours: @@ -759,6 +783,8 @@ public enum PremiumPerk: CaseIterable { return "Premium/Perk/MessageTags" case .business: return "Premium/Perk/Business" + case .messageEffects: + return "Premium/Perk/MessageEffects" case .businessLocation: return "Premium/BusinessPerk/Location" @@ -792,6 +818,7 @@ struct PremiumIntroConfiguration { .translation, .animatedEmoji, .emojiStatus, + .messageEffects, .messageTags, .colors, .wallpapers, @@ -1050,7 +1077,7 @@ final class SectionGroupComponent: Component { } } - func update(component: SectionGroupComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: SectionGroupComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let sideInset: CGFloat = 16.0 self.backgroundColor = component.backgroundColor @@ -1157,7 +1184,7 @@ final class SectionGroupComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -1830,18 +1857,19 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { UIColor(rgb: 0xef6922), UIColor(rgb: 0xe95a2c), UIColor(rgb: 0xe74e33), - UIColor(rgb: 0xe74e33), //replace + UIColor(rgb: 0xe74e33), UIColor(rgb: 0xe54937), UIColor(rgb: 0xe3433c), UIColor(rgb: 0xdb374b), UIColor(rgb: 0xcb3e6d), UIColor(rgb: 0xbc4395), UIColor(rgb: 0xab4ac4), + UIColor(rgb: 0xab4ac4), UIColor(rgb: 0xa34cd7), UIColor(rgb: 0x9b4fed), UIColor(rgb: 0x8958ff), UIColor(rgb: 0x676bff), - UIColor(rgb: 0x676bff), //replace + UIColor(rgb: 0x676bff), UIColor(rgb: 0x6172ff), UIColor(rgb: 0x5b79ff), UIColor(rgb: 0x4492ff), @@ -2066,6 +2094,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { demoSubject = .lastSeen case .messagePrivacy: demoSubject = .messagePrivacy + case .messageEffects: + demoSubject = .messageEffects case .business: demoSubject = .business let _ = ApplicationSpecificNotice.setDismissedBusinessBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() @@ -3621,7 +3651,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { context.add(bottomPanel .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0)) .opacity(bottomPanelAlpha) - .disappear(Transition.Disappear { view, transition, completion in + .disappear(ComponentTransition.Disappear { view, transition, completion in if case .none = transition.animation { completion() return @@ -3634,7 +3664,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { context.add(bottomSeparator .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height)) .opacity(bottomPanelAlpha) - .disappear(Transition.Disappear { view, transition, completion in + .disappear(ComponentTransition.Disappear { view, transition, completion in if case .none = transition.animation { completion() return @@ -3646,7 +3676,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { ) context.add(button .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height + bottomPanelPadding + button.size.height / 2.0)) - .disappear(Transition.Disappear { view, transition, completion in + .disappear(ComponentTransition.Disappear { view, transition, completion in if case .none = transition.animation { completion() return diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 81cb2778cb4..034163beb3e 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -307,7 +307,7 @@ public class PremiumLimitDisplayComponent: Component { } if let badgeText = component.badgeText { - let transition: Transition = .easeInOut(duration: from != nil ? 0.3 : 0.5) + let transition: ComponentTransition = .easeInOut(duration: from != nil ? 0.3 : 0.5) var frameTransition = transition if from == nil { frameTransition = frameTransition.withAnimation(.none) @@ -318,7 +318,7 @@ public class PremiumLimitDisplayComponent: Component { } var previousAvailableSize: CGSize? - func update(component: PremiumLimitDisplayComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: PremiumLimitDisplayComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor self.activeBackground.backgroundColor = component.activeColors.last?.cgColor @@ -493,7 +493,7 @@ public class PremiumLimitDisplayComponent: Component { } } - var progressTransition: Transition = .immediate + var progressTransition: ComponentTransition = .immediate if !transition.animation.isImmediate { progressTransition = .easeInOut(duration: 0.5) } @@ -738,7 +738,7 @@ public class PremiumLimitDisplayComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -1427,7 +1427,7 @@ private final class LimitSheetContent: CombinedComponent { ] } - var limitTransition: Transition = .immediate + var limitTransition: ComponentTransition = .immediate if boostUpdated { limitTransition = .easeInOut(duration: 0.35) } @@ -1513,11 +1513,11 @@ private final class LimitSheetContent: CombinedComponent { context.add(textChild .position(CGPoint(x: context.availableSize.width / 2.0, y: textOffset)) - .appear(Transition.Appear({ _, view, transition in + .appear(ComponentTransition.Appear({ _, view, transition in transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: 64.0), to: .zero, additive: true) transition.animateAlpha(view: view, from: 0.0, to: 1.0) })) - .disappear(Transition.Disappear({ view, transition, completion in + .disappear(ComponentTransition.Disappear({ view, transition, completion in view.superview?.sendSubviewToBack(view) transition.animatePosition(view: view, from: .zero, to: CGPoint(x: 0.0, y: -64.0), additive: true) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in @@ -1532,11 +1532,11 @@ private final class LimitSheetContent: CombinedComponent { context.add(alternateTextChild .position(CGPoint(x: context.availableSize.width / 2.0, y: textOffset)) - .appear(Transition.Appear({ _, view, transition in + .appear(ComponentTransition.Appear({ _, view, transition in transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: 64.0), to: .zero, additive: true) transition.animateAlpha(view: view, from: 0.0, to: 1.0) })) - .disappear(Transition.Disappear({ view, transition, completion in + .disappear(ComponentTransition.Disappear({ view, transition, completion in transition.animatePosition(view: view, from: .zero, to: CGPoint(x: 0.0, y: -64.0), additive: true) transition.setAlpha(view: view, alpha: 0.0, completion: { _ in completion() @@ -1938,7 +1938,7 @@ public final class BoostIconComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: BoostIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BoostIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -1986,7 +1986,7 @@ public final class BoostIconComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index 3c5ae8f0754..3f1fa037fd0 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -175,7 +175,7 @@ public class PremiumLimitsListScreen: ViewController { strongSelf.isPremium = isPremium strongSelf.promoConfiguration = promoConfiguration if !stickers.isEmpty { - strongSelf.updated(transition: Transition(.immediate).withUserData(DemoAnimateInTransition())) + strongSelf.updated(transition: ComponentTransition(.immediate).withUserData(DemoAnimateInTransition())) } }) } @@ -265,7 +265,7 @@ public class PremiumLimitsListScreen: ViewController { } private var dismissOffset: CGFloat? - func containerLayoutUpdated(layout: ContainerViewLayout, transition: Transition) { + func containerLayoutUpdated(layout: ContainerViewLayout, transition: ComponentTransition) { self.currentLayout = layout self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0)) @@ -356,7 +356,7 @@ public class PremiumLimitsListScreen: ViewController { } private var indexPosition: CGFloat? - func updated(transition: Transition) { + func updated(transition: ComponentTransition) { guard let controller = self.controller else { return } @@ -826,6 +826,25 @@ public class PremiumLimitsListScreen: ViewController { ) ) ) + availableItems[.messageEffects] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.messageEffects, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: context, + position: .top, + model: .island, + videoFile: videos["effects"], + decoration: .swirlStars + )), + title: strings.Premium_MessageEffects, + text: strings.Premium_MessageEffectsInfo, + textColor: textColor + ) + ) + ) + ) availableItems[.business] = DemoPagerComponent.Item( AnyComponentWithIdentity( id: PremiumDemoScreen.Subject.business, @@ -1320,11 +1339,11 @@ public class PremiumLimitsListScreen: ViewController { let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) - self.containerLayoutUpdated(layout: layout, transition: Transition(transition)) + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) } else { self.isExpanded = true - self.containerLayoutUpdated(layout: layout, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) } } else if scrollView != nil, (velocity.y < -300.0 || offset < topInset / 2.0) { if velocity.y > -2200.0 && velocity.y < -300.0, let listNode = listNode { @@ -1337,7 +1356,7 @@ public class PremiumLimitsListScreen: ViewController { let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) self.isExpanded = true - self.containerLayoutUpdated(layout: layout, transition: Transition(transition)) + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) } else { if let listNode = listNode { listNode.scroller.setContentOffset(CGPoint(), animated: false) @@ -1345,7 +1364,7 @@ public class PremiumLimitsListScreen: ViewController { scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) } - self.containerLayoutUpdated(layout: layout, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) } if !dismissing { @@ -1358,7 +1377,7 @@ public class PremiumLimitsListScreen: ViewController { case .cancelled: self.panGestureArguments = nil - self.containerLayoutUpdated(layout: layout, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) default: break } @@ -1383,7 +1402,7 @@ public class PremiumLimitsListScreen: ViewController { guard let layout = self.currentLayout else { return } - self.containerLayoutUpdated(layout: layout, transition: Transition(transition)) + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) } } @@ -1471,7 +1490,7 @@ public class PremiumLimitsListScreen: ViewController { self.currentLayout = layout super.containerLayoutUpdated(layout, transition: transition) - self.node.containerLayoutUpdated(layout: layout, transition: Transition(transition)) + self.node.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) } } diff --git a/submodules/PremiumUI/Sources/PremiumOptionComponent.swift b/submodules/PremiumUI/Sources/PremiumOptionComponent.swift index 23ec27e787c..df785dff42f 100644 --- a/submodules/PremiumUI/Sources/PremiumOptionComponent.swift +++ b/submodules/PremiumUI/Sources/PremiumOptionComponent.swift @@ -322,7 +322,7 @@ private final class CheckComponent: Component { } - func update(component: CheckComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: CheckComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.checkLayer.setSelected(component.selected, animated: true) self.checkLayer.theme = component.theme.checkNodeTheme @@ -334,7 +334,7 @@ private final class CheckComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift index fdf4c6fb874..95cf1a7e156 100644 --- a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift +++ b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift @@ -458,7 +458,7 @@ public class ReplaceBoostScreen: ViewController { self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) } - func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) { + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ComponentTransition) { let hadLayout = self.currentLayout != nil self.currentLayout = (layout, navigationHeight) @@ -761,11 +761,11 @@ public class ReplaceBoostScreen: ViewController { let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) } else { self.isExpanded = true - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) } } else if (velocity.y < -300.0 || offset < topInset / 2.0) { if velocity.y > -2200.0 && velocity.y < -300.0, let listNode = listNode { @@ -778,7 +778,7 @@ public class ReplaceBoostScreen: ViewController { let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) self.isExpanded = true - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) } else { if let listNode = listNode { listNode.scroller.setContentOffset(CGPoint(), animated: false) @@ -786,7 +786,7 @@ public class ReplaceBoostScreen: ViewController { scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) } - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) } if !dismissing { @@ -801,7 +801,7 @@ public class ReplaceBoostScreen: ViewController { case .cancelled: self.panGestureArguments = nil - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) self.updateFooterAlpha() default: @@ -818,7 +818,7 @@ public class ReplaceBoostScreen: ViewController { guard let (layout, navigationHeight) = self.currentLayout else { return } - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) } } @@ -985,7 +985,7 @@ public class ReplaceBoostScreen: ViewController { let navigationHeight: CGFloat = 56.0 - self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) } } @@ -1031,7 +1031,7 @@ private final class FooterView: UIView { let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - totalPanelHeight), size: CGSize(width: size.width, height: panelHeight)) - var buttonTransition: Transition = .easeInOut(duration: 0.2) + var buttonTransition: ComponentTransition = .easeInOut(duration: 0.2) if !hadLayout { buttonTransition = .immediate } @@ -1091,7 +1091,7 @@ private final class FooterView: UIView { return panelHeight } - func updateBackgroundAlpha(_ alpha: CGFloat, transition: Transition) { + func updateBackgroundAlpha(_ alpha: CGFloat, transition: ComponentTransition) { transition.setAlpha(view: self.backgroundNode.view, alpha: alpha) transition.setAlpha(view: self.separatorView, alpha: alpha) } @@ -1210,7 +1210,7 @@ private final class ReplaceBoostHeaderComponent: Component { } private var badgeImage: UIImage? - func update(component: ReplaceBoostHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ReplaceBoostHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -1305,7 +1305,7 @@ private final class ReplaceBoostHeaderComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/PremiumUI/Sources/StickersCarouselComponent.swift b/submodules/PremiumUI/Sources/StickersCarouselComponent.swift index 45dfae0797b..4b12336feac 100644 --- a/submodules/PremiumUI/Sources/StickersCarouselComponent.swift +++ b/submodules/PremiumUI/Sources/StickersCarouselComponent.swift @@ -45,7 +45,7 @@ final class StickersCarouselComponent: Component { private var component: StickersCarouselComponent? private var node: StickersCarouselNode? - public func update(component: StickersCarouselComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + public func update(component: StickersCarouselComponent, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { let isDisplaying = environment[DemoPageEnvironment.self].isDisplaying if self.node == nil && !component.stickers.isEmpty { @@ -79,7 +79,7 @@ final class StickersCarouselComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } diff --git a/submodules/PremiumUI/Sources/StoriesPageComponent.swift b/submodules/PremiumUI/Sources/StoriesPageComponent.swift index 6b37edbcfbb..396da5f2b19 100644 --- a/submodules/PremiumUI/Sources/StoriesPageComponent.swift +++ b/submodules/PremiumUI/Sources/StoriesPageComponent.swift @@ -57,7 +57,7 @@ private final class AvatarComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -106,7 +106,7 @@ private final class AvatarComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 3183090123a..d007db0582d 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -664,6 +664,9 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { } strongSelf.hideExpandedTopPanel = emojiContent.panelItemGroups.isEmpty + if emojiContent.panelItemGroups.count == 1 && emojiContent.panelItemGroups[0].groupId == AnyHashable("recent") { + strongSelf.hideExpandedTopPanel = true + } var emojiContent = emojiContent if let emojiSearchResult = emojiSearchState.result { @@ -701,11 +704,11 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { strongSelf.updateEmojiContent(emojiContent) if let reactionSelectionComponentHost = strongSelf.reactionSelectionComponentHost, let componentView = reactionSelectionComponentHost.view { - var emojiTransition: Transition = .immediate + var emojiTransition: ComponentTransition = .immediate if let scheduledEmojiContentAnimationHint = strongSelf.scheduledEmojiContentAnimationHint { strongSelf.scheduledEmojiContentAnimationHint = nil let contentAnimation = scheduledEmojiContentAnimationHint - emojiTransition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) + emojiTransition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) } var hideTopPanel = false @@ -1375,7 +1378,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { if (self.isExpanded || (self.reactionSelectionComponentHost != nil && !self.isCollapsing)), let _ = self.getEmojiContent, !self.reactionsLocked { let reactionSelectionComponentHost: ComponentView - var componentTransition = Transition(transition) + var componentTransition = ComponentTransition(transition) if let current = self.reactionSelectionComponentHost { reactionSelectionComponentHost = current } else { @@ -1390,7 +1393,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { if let scheduledEmojiContentAnimationHint = self.scheduledEmojiContentAnimationHint { self.scheduledEmojiContentAnimationHint = nil let contentAnimation = scheduledEmojiContentAnimationHint - componentTransition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) + componentTransition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) } var hideTopPanel = false @@ -1478,7 +1481,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { animationOffsetY += 46.0 + 54.0 - 4.0 } - Transition(transition).animateBoundsOrigin(view: mirrorContentClippingView, from: CGPoint(x: 0.0, y: animationOffsetY), to: CGPoint(), additive: true, completion: { [weak mirrorContentClippingView] _ in + ComponentTransition(transition).animateBoundsOrigin(view: mirrorContentClippingView, from: CGPoint(x: 0.0, y: animationOffsetY), to: CGPoint(), additive: true, completion: { [weak mirrorContentClippingView] _ in mirrorContentClippingView?.clipsToBounds = true }) } @@ -1931,7 +1934,6 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { resultGroups[groupIndex].items.append(resultItem) } else { resultGroupIndexById[groupId] = resultGroups.count - //TODO:localize resultGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: i == 0 ? nil : strings.Chat_MessageEffectMenu_SectionMessageEffects, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem])) } } @@ -2283,7 +2285,6 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { resultGroups[groupIndex].items.append(resultItem) } else { resultGroupIndexById[groupId] = resultGroups.count - //TODO:localize resultGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: i == 0 ? nil : strings.Chat_MessageEffectMenu_SectionMessageEffects, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem])) } } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index 9140f9745a2..180f7524c98 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -56,6 +56,25 @@ protocol ReactionItemNode: ASDisplayNode { private let lockedBackgroundImage: UIImage = generateFilledCircleImage(diameter: 16.0, color: .white)!.withRenderingMode(.alwaysTemplate) private let lockedBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white) +private final class StarsReactionEffectLayer: SimpleLayer { + override init() { + super.init() + + self.backgroundColor = UIColor.lightGray.withAlphaComponent(0.2).cgColor + } + + override init(layer: Any) { + super.init(layer: layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(size: CGSize) { + } +} + public final class ReactionNode: ASDisplayNode, ReactionItemNode { let context: AccountContext let theme: PresentationTheme @@ -69,6 +88,8 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { let selectionTintView: UIView? let selectionView: UIView? + private var starsEffectLayer: StarsReactionEffectLayer? + private var animateInAnimationNode: AnimatedStickerNode? private var staticAnimationPlaceholderView: UIImageView? private let staticAnimationNode: AnimatedStickerNode @@ -129,6 +150,12 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { super.init() + if case .custom(MessageReaction.starsReactionId) = item.reaction.rawValue { + let starsEffectLayer = StarsReactionEffectLayer() + self.starsEffectLayer = starsEffectLayer + self.layer.addSublayer(starsEffectLayer) + } + if item.stillAnimation.isCustomTemplateEmoji { if let animationNode = self.staticAnimationNode as? DefaultAnimatedStickerNodeImpl { animationNode.dynamicColor = theme.chat.inputPanel.panelControlAccentColor @@ -232,6 +259,11 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { public func updateLayout(size: CGSize, isExpanded: Bool, largeExpanded: Bool, isPreviewing: Bool, transition: ContainedViewLayoutTransition) { let intrinsicSize = size + if let starsEffectLayer = self.starsEffectLayer { + transition.updateFrame(layer: starsEffectLayer, frame: CGRect(origin: CGPoint(), size: size)) + starsEffectLayer.update(size: size) + } + let animationSize = self.item.stillAnimation.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) var animationDisplaySize = animationSize.aspectFitted(intrinsicSize) diff --git a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift index 4bbc99d2fe0..f220088e4dc 100644 --- a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift +++ b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift @@ -226,6 +226,12 @@ public func combineLatest(queue: Queue? = nil, _ s1: Signal, _ s2: Signal, _ s3: Signal, _ s4: Signal, _ s5: Signal, _ s6: Signal, _ s7: Signal, _ s8: Signal, _ s9: Signal, _ s10: Signal, _ s11: Signal, _ s12: Signal, _ s13: Signal, _ s14: Signal, _ s15: Signal, _ s16: Signal, _ s17: Signal, _ s18: Signal, _ s19: Signal, _ s20: Signal, _ s21: Signal, _ s22: Signal, _ s23: Signal, _ s24: Signal) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24), E> { + return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13), signalOfAny(s14), signalOfAny(s15), signalOfAny(s16), signalOfAny(s17), signalOfAny(s18), signalOfAny(s19), signalOfAny(s20), signalOfAny(s21), signalOfAny(s22), signalOfAny(s23), signalOfAny(s24)], combine: { values in + return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13, values[13] as! T14, values[14] as! T15, values[15] as! T16, values[16] as! T17, values[17] as! T18, values[18] as! T19, values[19] as! T20, values[20] as! T21, values[21] as! T22, values[22] as! T23, values[23] as! T24) + }, initialValues: [:], queue: queue) +} + public func combineLatest(queue: Queue? = nil, _ signals: [Signal]) -> Signal<[T], E> { if signals.count == 0 { return single([T](), E.self) diff --git a/submodules/ScreenCaptureDetection/Sources/ScreenCaptureDetection.swift b/submodules/ScreenCaptureDetection/Sources/ScreenCaptureDetection.swift index 167c3049550..f23aaf60bbc 100644 --- a/submodules/ScreenCaptureDetection/Sources/ScreenCaptureDetection.swift +++ b/submodules/ScreenCaptureDetection/Sources/ScreenCaptureDetection.swift @@ -98,7 +98,7 @@ public final class ScreenCaptureDetectionManager { } var value = value #if DEBUG - value = false + value = !"".isEmpty #endif strongSelf.isRecordingActive = value if value { diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index 37df41205dc..14baaadd015 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -999,7 +999,7 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList allItems.append(contentsOf: profileItems) let savedMessages = SettingsSearchableItem(id: .savedMessages(0), title: strings.Settings_SavedMessages, alternate: synonyms(strings.SettingsSearch_Synonyms_SavedMessages), icon: .savedMessages, breadcrumbs: [], present: { context, _, present in - present(.push, context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: context.account.peerId), subject: nil, botStart: nil, mode: .standard(.default))) + present(.push, context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: context.account.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)) }) allItems.append(savedMessages) @@ -1059,7 +1059,7 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList let _ = (context.engine.peers.supportPeerId() |> deliverOnMainQueue).start(next: { peerId in if let peerId = peerId { - present(.push, context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default))) + present(.push, context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)) } }) }) diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index abec599e04d..831a1c509a5 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -1043,8 +1043,8 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate private func contentNodeDidBeginDragging() { if let contentInfoView = self.contentInfoView, contentInfoView.alpha != 0.0 { - Transition.easeInOut(duration: 0.2).setAlpha(view: contentInfoView, alpha: 0.0) - Transition.easeInOut(duration: 0.2).setScale(view: contentInfoView, scale: 0.5) + ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: contentInfoView, alpha: 0.0) + ComponentTransition.easeInOut(duration: 0.2).setScale(view: contentInfoView, scale: 0.5) } } @@ -1350,8 +1350,8 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate self.animatingOut = true if let contentInfoView = self.contentInfoView, contentInfoView.alpha != 0.0 { - Transition.easeInOut(duration: 0.2).setAlpha(view: contentInfoView, alpha: 0.0) - Transition.easeInOut(duration: 0.2).setScale(view: contentInfoView, scale: 0.5) + ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: contentInfoView, alpha: 0.0) + ComponentTransition.easeInOut(duration: 0.2).setScale(view: contentInfoView, scale: 0.5) } if self.contentNode != nil { diff --git a/submodules/ShareItems/Sources/ShareItems.swift b/submodules/ShareItems/Sources/ShareItems.swift index 4742e79fad3..3ff8e54ca65 100644 --- a/submodules/ShareItems/Sources/ShareItems.swift +++ b/submodules/ShareItems/Sources/ShareItems.swift @@ -316,9 +316,9 @@ private func preparedShareItem(postbox: Postbox, network: Network, to peerId: Pe let disposable = TGShareLocationSignals.locationMessageContent(for: url).start(next: { value in if let value = value as? TGShareLocationResult { if let title = value.title { - subscriber.putNext(.done(.media(.media(.standalone(media: TelegramMediaMap(latitude: value.latitude, longitude: value.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: title, address: value.address, provider: value.provider, id: value.venueId, type: value.venueType), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)))))) + subscriber.putNext(.done(.media(.media(.standalone(media: TelegramMediaMap(latitude: value.latitude, longitude: value.longitude, heading: nil, accuracyRadius: nil, venue: MapVenue(title: title, address: value.address, provider: value.provider, id: value.venueId, type: value.venueType), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)))))) } else { - subscriber.putNext(.done(.media(.media(.standalone(media: TelegramMediaMap(latitude: value.latitude, longitude: value.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)))))) + subscriber.putNext(.done(.media(.media(.standalone(media: TelegramMediaMap(latitude: value.latitude, longitude: value.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)))))) } subscriber.putCompletion() } else if let value = value as? String { diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index ca308f94ffa..8e0a317db89 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -42,6 +42,7 @@ public protocol SparseItemGridBinding: AnyObject { func onTagTap() func didScroll() func coveringInsetOffsetUpdated(transition: ContainedViewLayoutTransition) + func scrollingOffsetUpdated(transition: ContainedViewLayoutTransition) func onBeginFastScrolling() func getShimmerColors() -> SparseItemGrid.ShimmerColors } @@ -1442,6 +1443,16 @@ public final class SparseItemGrid: ASDisplayNode { return 0.0 } } + + public var scrollingOffset: CGFloat { + if let currentViewportTransition = self.currentViewportTransition { + return currentViewportTransition.offset + } else if let currentViewport = self.currentViewport { + return currentViewport.offset + } else { + return 0.0 + } + } public var cancelExternalContentGestures: (() -> Void)? public var zoomLevelUpdated: ((ZoomLevel) -> Void)? @@ -1886,20 +1897,32 @@ public final class SparseItemGrid: ASDisplayNode { } private func offsetUpdated(viewport: Viewport, transition: ContainedViewLayoutTransition) { + guard let items = self.items else { + return + } + if self.currentViewportTransition != nil { return } + items.itemBinding.scrollingOffsetUpdated(transition: transition) + if let headerTextView = self.headerText?.view { headerTextView.layer.transform = CATransform3DMakeTranslation(0.0, -viewport.offset, 0.0) } } private func transitionOffsetUpdated(transition: ContainedViewLayoutTransition) { + guard let items = self.items else { + return + } + guard let currentViewportTransition = self.currentViewportTransition else { return } + items.itemBinding.scrollingOffsetUpdated(transition: transition) + if let headerTextView = self.headerText?.view { headerTextView.layer.transform = CATransform3DMakeTranslation(0.0, -currentViewportTransition.offset, 0.0) } diff --git a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift index a27fad04d59..b8068314b56 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift @@ -51,7 +51,7 @@ public final class MultilineText: Component { preconditionFailure() } - func update(component: MultilineText, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(component: MultilineText, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { self.text.attributedText = NSAttributedString(string: component.text, font: component.font, textColor: component.color, paragraphAlignment: nil) let textSize = self.text.updateLayout(availableSize) transition.setFrame(view: self.text.view, frame: CGRect(origin: CGPoint(), size: textSize)) @@ -64,7 +64,7 @@ public final class MultilineText: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } @@ -97,7 +97,7 @@ public final class LottieAnimationComponent: Component { preconditionFailure() } - func update(component: LottieAnimationComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(component: LottieAnimationComponent, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { if self.currentName != component.name { self.currentName = component.name @@ -126,7 +126,7 @@ public final class LottieAnimationComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } @@ -184,7 +184,7 @@ private final class ScrollingTooltipAnimationComponent: Component { self.animator = animator } - func update(component: ScrollingTooltipAnimationComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(component: ScrollingTooltipAnimationComponent, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { return CGSize(width: 32.0, height: 32.0) } @@ -249,7 +249,7 @@ private final class ScrollingTooltipAnimationComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } @@ -324,7 +324,7 @@ public final class TooltipComponent: Component { preconditionFailure() } - func update(component: TooltipComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(component: TooltipComponent, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { let insets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0) let spacing: CGFloat = 8.0 @@ -400,7 +400,7 @@ public final class TooltipComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } @@ -437,7 +437,7 @@ private final class RoundedRectangle: Component { preconditionFailure() } - func update(component: RoundedRectangle, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(component: RoundedRectangle, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { let shadowInset: CGFloat = 0.0 let diameter = min(availableSize.width, availableSize.height) @@ -476,7 +476,7 @@ private final class RoundedRectangle: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } @@ -514,7 +514,7 @@ private final class ShadowRoundedRectangle: Component { preconditionFailure() } - func update(component: ShadowRoundedRectangle, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(component: ShadowRoundedRectangle, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { let diameter = min(availableSize.width, availableSize.height) var updated = false @@ -551,7 +551,7 @@ private final class ShadowRoundedRectangle: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } @@ -702,7 +702,7 @@ public final class RollingText: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize) } } @@ -1218,12 +1218,12 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { } let lineIndicatorSize = CGSize(width: (self.isDragging || self.lineTooltip != nil) ? 6.0 : 3.0, height: scrollIndicatorHeight) - let mappedTransition: Transition + let mappedTransition: ComponentTransition switch transition { case .immediate: mappedTransition = .immediate case let .animated(duration, _): - mappedTransition = Transition(animation: .curve(duration: duration, curve: .easeInOut)) + mappedTransition = ComponentTransition(animation: .curve(duration: duration, curve: .easeInOut)) } let _ = self.lineIndicator.update( transition: mappedTransition, diff --git a/submodules/StatisticsUI/BUILD b/submodules/StatisticsUI/BUILD index acaa5c567d2..c6c349766d1 100644 --- a/submodules/StatisticsUI/BUILD +++ b/submodules/StatisticsUI/BUILD @@ -43,11 +43,18 @@ swift_library( "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/Components/SheetComponent", + "//submodules/Components/BundleIconComponent", "//submodules/Components/MultilineTextComponent", "//submodules/Components/MultilineTextWithEntitiesComponent", "//submodules/TelegramNotices", "//submodules/UIKitRuntimeUtils", - "//submodules/PeerInfoUI", + "//submodules/PasswordSetupUI", + "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", + "//submodules/TelegramUI/Components/ListItemComponentAdaptor", + "//submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/Stars/StarsAvatarComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/StatisticsUI/Sources/BoostHeaderItem.swift b/submodules/StatisticsUI/Sources/BoostHeaderItem.swift index 5b70e642fc4..34f192ec510 100644 --- a/submodules/StatisticsUI/Sources/BoostHeaderItem.swift +++ b/submodules/StatisticsUI/Sources/BoostHeaderItem.swift @@ -206,7 +206,7 @@ final class BoostHeaderItemNode: ItemListControllerHeaderItemNode { if let hostView = self.hostView { let size = hostView.update( - transition: Transition(transition), + transition: ComponentTransition(transition), component: component, environment: {}, containerSize: containerSize diff --git a/submodules/StatisticsUI/Sources/BoostsTabsItem.swift b/submodules/StatisticsUI/Sources/BoostsTabsItem.swift index 6a71df18c57..538b32351ce 100644 --- a/submodules/StatisticsUI/Sources/BoostsTabsItem.swift +++ b/submodules/StatisticsUI/Sources/BoostsTabsItem.swift @@ -242,9 +242,9 @@ private final class BoostsTabsItemNode: ListViewItemNode { switch item.selectedTab { case .boosts: - selectionFrame = CGRect(x: strongSelf.boostsTextNode.frame.minX, y: layoutSize.height - selectionHeight, width: strongSelf.boostsTextNode.frame.width, height: selectionHeight) + selectionFrame = CGRect(x: strongSelf.boostsTextNode.frame.minX, y: contentSize.height - selectionHeight, width: strongSelf.boostsTextNode.frame.width, height: selectionHeight) case .gifts: - selectionFrame = CGRect(x: strongSelf.giftsTextNode.frame.minX, y: layoutSize.height - selectionHeight, width: strongSelf.giftsTextNode.frame.width, height: selectionHeight) + selectionFrame = CGRect(x: strongSelf.giftsTextNode.frame.minX, y: contentSize.height - selectionHeight, width: strongSelf.giftsTextNode.frame.width, height: selectionHeight) } var transition: ContainedViewLayoutTransition = .immediate diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 222303780aa..eec5c35f63e 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -25,6 +25,7 @@ import StoryContainerScreen import TelegramNotices import ComponentFlow import BoostLevelIconComponent +import StarsWithdrawalScreen private let initialBoostersDisplayedLimit: Int32 = 5 private let initialTransactionsDisplayedLimit: Int32 = 5 @@ -42,17 +43,22 @@ private final class ChannelStatsControllerArguments { let openGifts: () -> Void let createPrepaidGiveaway: (PrepaidGiveaway) -> Void let updateGiftsSelected: (Bool) -> Void + let updateStarsSelected: (Bool) -> Void - let requestWithdraw: () -> Void + let requestTonWithdraw: () -> Void + let requestStarsWithdraw: () -> Void + let showTimeoutTooltip: (Int32) -> Void + let buyAds: () -> Void let openMonetizationIntro: () -> Void let openMonetizationInfo: () -> Void - let openTransaction: (RevenueStatsTransactionsContext.State.Transaction) -> Void - let expandTransactions: () -> Void + let openTonTransaction: (RevenueStatsTransactionsContext.State.Transaction) -> Void + let openStarsTransaction: (StarsContext.State.Transaction) -> Void + let expandTransactions: (Bool) -> Void let updateCpmEnabled: (Bool) -> Void let presentCpmLocked: () -> Void let dismissInput: () -> Void - init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal, openPostStats: @escaping (EnginePeer, StatsPostItem) -> Void, openStory: @escaping (EngineStoryItem, UIView) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void, requestWithdraw: @escaping () -> Void, openMonetizationIntro: @escaping () -> Void, openMonetizationInfo: @escaping () -> Void, openTransaction: @escaping (RevenueStatsTransactionsContext.State.Transaction) -> Void, expandTransactions: @escaping () -> Void, updateCpmEnabled: @escaping (Bool) -> Void, presentCpmLocked: @escaping () -> Void, dismissInput: @escaping () -> Void) { + init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal, openPostStats: @escaping (EnginePeer, StatsPostItem) -> Void, openStory: @escaping (EngineStoryItem, UIView) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void, updateStarsSelected: @escaping (Bool) -> Void, requestTonWithdraw: @escaping () -> Void, requestStarsWithdraw: @escaping () -> Void, showTimeoutTooltip: @escaping (Int32) -> Void, buyAds: @escaping () -> Void, openMonetizationIntro: @escaping () -> Void, openMonetizationInfo: @escaping () -> Void, openTonTransaction: @escaping (RevenueStatsTransactionsContext.State.Transaction) -> Void, openStarsTransaction: @escaping (StarsContext.State.Transaction) -> Void, expandTransactions: @escaping (Bool) -> Void, updateCpmEnabled: @escaping (Bool) -> Void, presentCpmLocked: @escaping () -> Void, dismissInput: @escaping () -> Void) { self.context = context self.loadDetailedGraph = loadDetailedGraph self.openPostStats = openPostStats @@ -65,10 +71,15 @@ private final class ChannelStatsControllerArguments { self.openGifts = openGifts self.createPrepaidGiveaway = createPrepaidGiveaway self.updateGiftsSelected = updateGiftsSelected - self.requestWithdraw = requestWithdraw + self.updateStarsSelected = updateStarsSelected + self.requestTonWithdraw = requestTonWithdraw + self.requestStarsWithdraw = requestStarsWithdraw + self.showTimeoutTooltip = showTimeoutTooltip + self.buyAds = buyAds self.openMonetizationIntro = openMonetizationIntro self.openMonetizationInfo = openMonetizationInfo - self.openTransaction = openTransaction + self.openTonTransaction = openTonTransaction + self.openStarsTransaction = openStarsTransaction self.expandTransactions = expandTransactions self.updateCpmEnabled = updateCpmEnabled self.presentCpmLocked = presentCpmLocked @@ -101,9 +112,11 @@ private enum StatsSection: Int32 { case adsHeader case adsImpressions - case adsRevenue + case adsTonRevenue + case adsStarsRevenue case adsProceeds - case adsBalance + case adsTonBalance + case adsStarsBalance case adsTransactions case adsCpm } @@ -218,19 +231,28 @@ private enum StatsEntry: ItemListNodeEntry { case adsImpressionsTitle(PresentationTheme, String) case adsImpressionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) - case adsRevenueTitle(PresentationTheme, String) - case adsRevenueGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType, Double) + case adsTonRevenueTitle(PresentationTheme, String) + case adsTonRevenueGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType, Double) + + case adsStarsRevenueTitle(PresentationTheme, String) + case adsStarsRevenueGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType, Double) case adsProceedsTitle(PresentationTheme, String) - case adsProceedsOverview(PresentationTheme, RevenueStats, TelegramMediaFile?) + case adsProceedsOverview(PresentationTheme, RevenueStats, StarsRevenueStats?) - case adsBalanceTitle(PresentationTheme, String) - case adsBalance(PresentationTheme, RevenueStats, Bool, Bool, TelegramMediaFile?) - case adsBalanceInfo(PresentationTheme, String) + case adsTonBalanceTitle(PresentationTheme, String) + case adsTonBalance(PresentationTheme, RevenueStats, Bool, Bool) + case adsTonBalanceInfo(PresentationTheme, String) + + case adsStarsBalanceTitle(PresentationTheme, String) + case adsStarsBalance(PresentationTheme, StarsRevenueStats, Bool, Bool, Int32?) + case adsStarsBalanceInfo(PresentationTheme, String) case adsTransactionsTitle(PresentationTheme, String) + case adsTransactionsTabs(PresentationTheme, String, String, Bool) case adsTransaction(Int32, PresentationTheme, RevenueStatsTransactionsContext.State.Transaction) - case adsTransactionsExpand(PresentationTheme, String) + case adsStarsTransaction(Int32, PresentationTheme, StarsContext.State.Transaction) + case adsTransactionsExpand(PresentationTheme, String, Bool) case adsCpmToggle(PresentationTheme, String, Int32, Bool?) case adsCpmInfo(PresentationTheme, String) @@ -281,13 +303,17 @@ private enum StatsEntry: ItemListNodeEntry { return StatsSection.adsHeader.rawValue case .adsImpressionsTitle, .adsImpressionsGraph: return StatsSection.adsImpressions.rawValue - case .adsRevenueTitle, .adsRevenueGraph: - return StatsSection.adsRevenue.rawValue + case .adsTonRevenueTitle, .adsTonRevenueGraph: + return StatsSection.adsTonRevenue.rawValue + case .adsStarsRevenueTitle, .adsStarsRevenueGraph: + return StatsSection.adsStarsRevenue.rawValue case .adsProceedsTitle, .adsProceedsOverview: return StatsSection.adsProceeds.rawValue - case .adsBalanceTitle, .adsBalance, .adsBalanceInfo: - return StatsSection.adsBalance.rawValue - case .adsTransactionsTitle, .adsTransaction, .adsTransactionsExpand: + case .adsTonBalanceTitle, .adsTonBalance, .adsTonBalanceInfo: + return StatsSection.adsTonBalance.rawValue + case .adsStarsBalanceTitle, .adsStarsBalance, .adsStarsBalanceInfo: + return StatsSection.adsStarsBalance.rawValue + case .adsTransactionsTitle, .adsTransactionsTabs, .adsTransaction, .adsStarsTransaction, .adsTransactionsExpand: return StatsSection.adsTransactions.rawValue case .adsCpmToggle, .adsCpmInfo: return StatsSection.adsCpm.rawValue @@ -392,30 +418,44 @@ private enum StatsEntry: ItemListNodeEntry { return 20001 case .adsImpressionsGraph: return 20002 - case .adsRevenueTitle: + case .adsTonRevenueTitle: return 20003 - case .adsRevenueGraph: + case .adsTonRevenueGraph: return 20004 - case .adsProceedsTitle: + case .adsStarsRevenueTitle: return 20005 - case .adsProceedsOverview: + case .adsStarsRevenueGraph: return 20006 - case .adsBalanceTitle: + case .adsProceedsTitle: return 20007 - case .adsBalance: + case .adsProceedsOverview: return 20008 - case .adsBalanceInfo: + case .adsTonBalanceTitle: return 20009 - case .adsTransactionsTitle: + case .adsTonBalance: return 20010 + case .adsTonBalanceInfo: + return 20011 + case .adsStarsBalanceTitle: + return 20012 + case .adsStarsBalance: + return 20013 + case .adsStarsBalanceInfo: + return 20014 + case .adsTransactionsTitle: + return 20015 + case .adsTransactionsTabs: + return 20016 case let .adsTransaction(index, _, _): - return 20011 + index + return 20017 + index + case let .adsStarsTransaction(index, _, _): + return 30017 + index case .adsTransactionsExpand: - return 30000 + return 40000 case .adsCpmToggle: - return 30001 + return 40001 case .adsCpmInfo: - return 30002 + return 40002 } } @@ -709,14 +749,26 @@ private enum StatsEntry: ItemListNodeEntry { } else { return false } - case let .adsRevenueTitle(lhsTheme, lhsText): - if case let .adsRevenueTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .adsTonRevenueTitle(lhsTheme, lhsText): + if case let .adsTonRevenueTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .adsRevenueGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType, lhsRate): - if case let .adsRevenueGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType, rhsRate) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType, lhsRate == rhsRate { + case let .adsTonRevenueGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType, lhsRate): + if case let .adsTonRevenueGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType, rhsRate) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType, lhsRate == rhsRate { + return true + } else { + return false + } + case let .adsStarsRevenueTitle(lhsTheme, lhsText): + if case let .adsStarsRevenueTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .adsStarsRevenueGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType, lhsRate): + if case let .adsStarsRevenueGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType, rhsRate) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType, lhsRate == rhsRate { return true } else { return false @@ -727,26 +779,44 @@ private enum StatsEntry: ItemListNodeEntry { } else { return false } - case let .adsProceedsOverview(lhsTheme, lhsStatus, lhsAnimatedEmoji): - if case let .adsProceedsOverview(rhsTheme, rhsStatus, rhsAnimatedEmoji) = rhs, lhsTheme === rhsTheme, lhsStatus == rhsStatus, lhsAnimatedEmoji == rhsAnimatedEmoji { + case let .adsProceedsOverview(lhsTheme, lhsStatus, lhsStarsStatus): + if case let .adsProceedsOverview(rhsTheme, rhsStatus, rhsStarsStatus) = rhs, lhsTheme === rhsTheme, lhsStatus == rhsStatus, lhsStarsStatus == rhsStarsStatus { return true } else { return false } - case let .adsBalanceTitle(lhsTheme, lhsText): - if case let .adsBalanceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .adsTonBalanceTitle(lhsTheme, lhsText): + if case let .adsTonBalanceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .adsBalance(lhsTheme, lhsStats, lhsCanWithdraw, lhsIsEnabled, lhsAnimatedEmoji): - if case let .adsBalance(rhsTheme, rhsStats, rhsCanWithdraw, rhsIsEnabled, rhsAnimatedEmoji) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsCanWithdraw == rhsCanWithdraw, lhsIsEnabled == rhsIsEnabled, lhsAnimatedEmoji == rhsAnimatedEmoji { + case let .adsTonBalance(lhsTheme, lhsStats, lhsCanWithdraw, lhsIsEnabled): + if case let .adsTonBalance(rhsTheme, rhsStats, rhsCanWithdraw, rhsIsEnabled) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsCanWithdraw == rhsCanWithdraw, lhsIsEnabled == rhsIsEnabled { return true } else { return false } - case let .adsBalanceInfo(lhsTheme, lhsText): - if case let .adsBalanceInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .adsTonBalanceInfo(lhsTheme, lhsText): + if case let .adsTonBalanceInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .adsStarsBalanceTitle(lhsTheme, lhsText): + if case let .adsStarsBalanceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .adsStarsBalance(lhsTheme, lhsStats, lhsCanWithdraw, lhsIsEnabled, lhsCooldownUntilTimestamp): + if case let .adsStarsBalance(rhsTheme, rhsStats, rhsCanWithdraw, rhsIsEnabled, rhsCooldownUntilTimestamp) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsCanWithdraw == rhsCanWithdraw, lhsIsEnabled == rhsIsEnabled, lhsCooldownUntilTimestamp == rhsCooldownUntilTimestamp { + return true + } else { + return false + } + case let .adsStarsBalanceInfo(lhsTheme, lhsText): + if case let .adsStarsBalanceInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -757,14 +827,26 @@ private enum StatsEntry: ItemListNodeEntry { } else { return false } + case let .adsTransactionsTabs(lhsTheme, lhsTonText, lhsStarsText, lhsStarsSelected): + if case let .adsTransactionsTabs(rhsTheme, rhsTonText, rhsStarsText, rhsStarsSelected) = rhs, lhsTheme === rhsTheme, lhsTonText == rhsTonText, lhsStarsText == rhsStarsText, lhsStarsSelected == rhsStarsSelected { + return true + } else { + return false + } case let .adsTransaction(lhsIndex, lhsTheme, lhsTransaction): if case let .adsTransaction(rhsIndex, rhsTheme, rhsTransaction) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTransaction == rhsTransaction { return true } else { return false } - case let .adsTransactionsExpand(lhsTheme, lhsText): - if case let .adsTransactionsExpand(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .adsStarsTransaction(lhsIndex, lhsTheme, lhsTransaction): + if case let .adsStarsTransaction(rhsIndex, rhsTheme, rhsTransaction) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTransaction == rhsTransaction { + return true + } else { + return false + } + case let .adsTransactionsExpand(lhsTheme, lhsText, lhsStars): + if case let .adsTransactionsExpand(rhsTheme, rhsText, rhsStars) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsStars == rhsStars { return true } else { return false @@ -811,9 +893,11 @@ private enum StatsEntry: ItemListNodeEntry { let .boostersTitle(_, text), let .boostLinkTitle(_, text), let .adsImpressionsTitle(_, text), - let .adsRevenueTitle(_, text), + let .adsTonRevenueTitle(_, text), + let .adsStarsRevenueTitle(_, text), let .adsProceedsTitle(_, text), - let .adsBalanceTitle(_, text), + let .adsTonBalanceTitle(_, text), + let .adsStarsBalanceTitle(_, text), let .adsTransactionsTitle(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .boostPrepaidInfo(_, text), @@ -835,7 +919,9 @@ private enum StatsEntry: ItemListNodeEntry { let .storyReactionsByEmotionGraph(_, _, _, graph, type), let .adsImpressionsGraph(_, _, _, graph, type): return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks) - case let .adsRevenueGraph(_, _, _, graph, type, rate): + case let .adsTonRevenueGraph(_, _, _, graph, type, rate): + return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, conversionRate: rate, sectionId: self.section, style: .blocks) + case let .adsStarsRevenueGraph(_, _, _, graph, type, rate): return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, conversionRate: rate, sectionId: self.section, style: .blocks) case let .postInteractionsGraph(_, _, _, graph, type), let .instantPageInteractionsGraph(_, _, _, graph, type), @@ -959,25 +1045,64 @@ private enum StatsEntry: ItemListNodeEntry { return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in arguments.openMonetizationIntro() }) - case let .adsProceedsOverview(_, stats, animatedEmoji): - return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: false, stats: stats, animatedEmoji: animatedEmoji, sectionId: self.section, style: .blocks) - case let .adsBalance(_, stats, canWithdraw, isEnabled, _): + case let .adsProceedsOverview(_, stats, starsStats): + return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: false, stats: stats, additionalStats: starsStats, sectionId: self.section, style: .blocks) + case let .adsTonBalance(_, stats, canWithdraw, isEnabled): return MonetizationBalanceItem( context: arguments.context, presentationData: presentationData, stats: stats, canWithdraw: canWithdraw, isEnabled: isEnabled, + actionCooldownUntilTimestamp: nil, withdrawAction: { - arguments.requestWithdraw() + arguments.requestTonWithdraw() }, + buyAdsAction: nil, sectionId: self.section, style: .blocks ) - case let .adsBalanceInfo(_, text): + case let .adsTonBalanceInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in arguments.openMonetizationInfo() }) + case let .adsStarsBalance(_, stats, canWithdraw, isEnabled, cooldownUntilTimestamp): + return MonetizationBalanceItem( + context: arguments.context, + presentationData: presentationData, + stats: stats, + canWithdraw: canWithdraw, + isEnabled: isEnabled, + actionCooldownUntilTimestamp: cooldownUntilTimestamp, + withdrawAction: { + var remainingCooldownSeconds: Int32 = 0 + if let cooldownUntilTimestamp { + remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) + remainingCooldownSeconds = max(0, remainingCooldownSeconds) + + if remainingCooldownSeconds > 0 { + arguments.showTimeoutTooltip(cooldownUntilTimestamp) + } else { + arguments.requestStarsWithdraw() + } + } else { + arguments.requestStarsWithdraw() + } + }, + buyAdsAction: canWithdraw ? { + arguments.buyAds() + } : nil, + sectionId: self.section, + style: .blocks + ) + case let .adsStarsBalanceInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in + arguments.openMonetizationInfo() + }) + case let .adsTransactionsTabs(_, tonText, starsText, starsSelected): + return BoostsTabsItem(theme: presentationData.theme, boostsText: tonText, giftsText: starsText, selectedTab: starsSelected ? .gifts : .boosts, sectionId: self.section, selectionUpdated: { tab in + arguments.updateStarsSelected(tab == .gifts) + }) case let .adsTransaction(_, theme, transaction): let font = Font.with(size: floor(presentationData.fontSize.itemListBaseFontSize)) let smallLabelFont = Font.with(size: floor(presentationData.fontSize.itemListBaseFontSize / 17.0 * 13.0)) @@ -1024,11 +1149,15 @@ private enum StatsEntry: ItemListNodeEntry { } return ItemListDisclosureItem(presentationData: presentationData, title: "", attributedTitle: title, label: "", attributedLabel: label, labelStyle: .coloredText(labelColor), additionalDetailLabel: detailText, additionalDetailLabelColor: detailColor, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { - arguments.openTransaction(transaction) + arguments.openTonTransaction(transaction) }) - case let .adsTransactionsExpand(theme, title): + case let .adsStarsTransaction(_, _, transaction): + return StarsTransactionItem(context: arguments.context, presentationData: presentationData, transaction: transaction, action: { + arguments.openStarsTransaction(transaction) + }, sectionId: self.section, style: .blocks) + case let .adsTransactionsExpand(theme, title, stars): return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: { - arguments.expandTransactions() + arguments.expandTransactions(stars) }) case let .adsCpmToggle(_, title, minLevel, value): var badgeComponent: AnyComponent? @@ -1062,23 +1191,26 @@ private struct ChannelStatsControllerState: Equatable { let boostersExpanded: Bool let moreBoostersDisplayed: Int32 let giftsSelected: Bool + let starsSelected: Bool let transactionsExpanded: Bool let moreTransactionsDisplayed: Int32 - + init() { self.section = .stats self.boostersExpanded = false self.moreBoostersDisplayed = 0 self.giftsSelected = false + self.starsSelected = false self.transactionsExpanded = false self.moreTransactionsDisplayed = 0 } - init(section: ChannelStatsSection, boostersExpanded: Bool, moreBoostersDisplayed: Int32, giftsSelected: Bool, transactionsExpanded: Bool, moreTransactionsDisplayed: Int32) { + init(section: ChannelStatsSection, boostersExpanded: Bool, moreBoostersDisplayed: Int32, giftsSelected: Bool, starsSelected: Bool, transactionsExpanded: Bool, moreTransactionsDisplayed: Int32) { self.section = section self.boostersExpanded = boostersExpanded self.moreBoostersDisplayed = moreBoostersDisplayed self.giftsSelected = giftsSelected + self.starsSelected = starsSelected self.transactionsExpanded = transactionsExpanded self.moreTransactionsDisplayed = moreTransactionsDisplayed } @@ -1096,6 +1228,9 @@ private struct ChannelStatsControllerState: Equatable { if lhs.giftsSelected != rhs.giftsSelected { return false } + if lhs.starsSelected != rhs.starsSelected { + return false + } if lhs.transactionsExpanded != rhs.transactionsExpanded { return false } @@ -1106,27 +1241,31 @@ private struct ChannelStatsControllerState: Equatable { } func withUpdatedSection(_ section: ChannelStatsSection) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) + return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) } func withUpdatedBoostersExpanded(_ boostersExpanded: Bool) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) + return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) } func withUpdatedMoreBoostersDisplayed(_ moreBoostersDisplayed: Int32) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) + return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) } func withUpdatedGiftsSelected(_ giftsSelected: Bool) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) + return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) + } + + func withUpdatedStarsSelected(_ starsSelected: Bool) -> ChannelStatsControllerState { + return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) } func withUpdatedTransactionsExpanded(_ transactionsExpanded: Bool) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) + return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: transactionsExpanded, moreTransactionsDisplayed: self.moreTransactionsDisplayed) } func withUpdatedMoreTransactionsDisplayed(_ moreTransactionsDisplayed: Int32) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: moreTransactionsDisplayed) + return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, starsSelected: self.starsSelected, transactionsExpanded: self.transactionsExpanded, moreTransactionsDisplayed: moreTransactionsDisplayed) } } @@ -1135,7 +1274,7 @@ private func statsEntries( data: ChannelStats, peer: EnginePeer?, messages: [Message]?, - stories: PeerStoryListContext.State?, + stories: StoryListContext.State?, interactions: [ChannelStatsPostInteractions.PostId: ChannelStatsPostInteractions]? ) -> [StatsEntry] { var entries: [StatsEntry] = [] @@ -1217,8 +1356,8 @@ private func statsEntries( } if let stories { for story in stories.items { - if let _ = interactions[.story(peerId: peer.id, id: story.id)] { - posts.append(.story(peer, story)) + if let _ = interactions[.story(peerId: peer.id, id: story.storyItem.id)] { + posts.append(.story(peer, story.storyItem)) } } } @@ -1373,13 +1512,12 @@ private func monetizationEntries( data: RevenueStats, boostData: ChannelBoostStatus?, transactionsInfo: RevenueStatsTransactionsContext.State, + starsData: StarsRevenueStats?, + starsTransactionsInfo: StarsTransactionsContext.State, adsRestricted: Bool, - animatedEmojis: [String: [StickerPackItem]], premiumConfiguration: PremiumConfiguration, monetizationConfiguration: MonetizationConfiguration ) -> [StatsEntry] { - let diamond = animatedEmojis["💎"]?.first?.file - var entries: [StatsEntry] = [] entries.append(.adsHeader(presentationData.theme, presentationData.strings.Monetization_Header)) @@ -1389,19 +1527,24 @@ private func monetizationEntries( } if !data.revenueGraph.isEmpty { - entries.append(.adsRevenueTitle(presentationData.theme, presentationData.strings.Monetization_AdRevenueTitle)) - entries.append(.adsRevenueGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.revenueGraph, .currency, data.usdRate)) + entries.append(.adsTonRevenueTitle(presentationData.theme, presentationData.strings.Monetization_AdRevenueTitle)) + entries.append(.adsTonRevenueGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.revenueGraph, .currency, data.usdRate)) + } + + if let starsData, !starsData.revenueGraph.isEmpty { + entries.append(.adsStarsRevenueTitle(presentationData.theme, presentationData.strings.Monetization_StarsRevenueTitle)) + entries.append(.adsStarsRevenueGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, starsData.revenueGraph, .stars, starsData.usdRate)) } entries.append(.adsProceedsTitle(presentationData.theme, presentationData.strings.Monetization_OverviewTitle)) - entries.append(.adsProceedsOverview(presentationData.theme, data, diamond)) + entries.append(.adsProceedsOverview(presentationData.theme, data, starsData)) var isCreator = false if let peer, case let .channel(channel) = peer, channel.flags.contains(.isCreator) { isCreator = true } - entries.append(.adsBalanceTitle(presentationData.theme, presentationData.strings.Monetization_BalanceTitle)) - entries.append(.adsBalance(presentationData.theme, data, isCreator && data.balances.availableBalance > 0, monetizationConfiguration.withdrawalAvailable, diamond)) + entries.append(.adsTonBalanceTitle(presentationData.theme, presentationData.strings.Monetization_TonBalanceTitle)) + entries.append(.adsTonBalance(presentationData.theme, data, isCreator && data.balances.availableBalance > 0, monetizationConfiguration.withdrawalAvailable)) if isCreator { let withdrawalInfoText: String @@ -1412,11 +1555,35 @@ private func monetizationEntries( } else { withdrawalInfoText = presentationData.strings.Monetization_Balance_ComingLaterInfo } - entries.append(.adsBalanceInfo(presentationData.theme, withdrawalInfoText)) + entries.append(.adsTonBalanceInfo(presentationData.theme, withdrawalInfoText)) } - - if !transactionsInfo.transactions.isEmpty { - entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_TransactionsTitle)) + + if let starsData, starsData.balances.overallRevenue > 0 { + entries.append(.adsStarsBalanceTitle(presentationData.theme, presentationData.strings.Monetization_StarsBalanceTitle)) + entries.append(.adsStarsBalance(presentationData.theme, starsData, isCreator && starsData.balances.availableBalance > 0, starsData.balances.withdrawEnabled, starsData.balances.nextWithdrawalTimestamp)) + entries.append(.adsStarsBalanceInfo(presentationData.theme, presentationData.strings.Monetization_Balance_StarsInfo)) + } + + var addedTransactionsTabs = false + if !transactionsInfo.transactions.isEmpty && !starsTransactionsInfo.transactions.isEmpty { + addedTransactionsTabs = true + entries.append(.adsTransactionsTabs(presentationData.theme, presentationData.strings.Monetization_TonTransactions, presentationData.strings.Monetization_StarsTransactions, state.starsSelected)) + } + + var displayTonTransactions = false + if !transactionsInfo.transactions.isEmpty && (starsTransactionsInfo.transactions.isEmpty || !state.starsSelected) { + displayTonTransactions = true + } + + var displayStarsTransactions = false + if !starsTransactionsInfo.transactions.isEmpty && (transactionsInfo.transactions.isEmpty || state.starsSelected) { + displayStarsTransactions = true + } + + if displayTonTransactions { + if !addedTransactionsTabs { + entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_TonTransactions.uppercased())) + } var transactions = transactionsInfo.transactions var limit: Int32 @@ -1438,9 +1605,40 @@ private func monetizationEntries( if !state.transactionsExpanded { moreCount = min(20, transactionsInfo.count - Int32(transactions.count)) } else { - moreCount = min(500, transactionsInfo.count - Int32(transactions.count)) + moreCount = min(50, transactionsInfo.count - Int32(transactions.count)) } - entries.append(.adsTransactionsExpand(presentationData.theme, presentationData.strings.Monetization_Transaction_ShowMoreTransactions(moreCount))) + entries.append(.adsTransactionsExpand(presentationData.theme, presentationData.strings.Monetization_Transaction_ShowMoreTransactions(moreCount), false)) + } + } + + if displayStarsTransactions { + if !addedTransactionsTabs { + entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_StarsTransactions.uppercased())) + } + + var transactions = starsTransactionsInfo.transactions + var limit: Int32 + if state.transactionsExpanded { + limit = 25 + state.moreTransactionsDisplayed + } else { + limit = initialTransactionsDisplayedLimit + } + transactions = Array(transactions.prefix(Int(limit))) + + var i: Int32 = 0 + for transaction in transactions { + entries.append(.adsStarsTransaction(i, presentationData.theme, transaction)) + i += 1 + } + + if starsTransactionsInfo.canLoadMore || starsTransactionsInfo.transactions.count > transactions.count { + let moreCount: Int32 + if !state.transactionsExpanded { + moreCount = min(20, Int32(starsTransactionsInfo.transactions.count - transactions.count)) + } else { + moreCount = min(50, Int32(starsTransactionsInfo.transactions.count - transactions.count)) + } + entries.append(.adsTransactionsExpand(presentationData.theme, presentationData.strings.Monetization_Transaction_ShowMoreTransactions(moreCount), true)) } } @@ -1463,17 +1661,18 @@ private func channelStatsControllerEntries( peer: EnginePeer?, data: ChannelStats?, messages: [Message]?, - stories: PeerStoryListContext.State?, - interactions: [ChannelStatsPostInteractions.PostId: ChannelStatsPostInteractions]?, + stories: StoryListContext.State?, + interactions: [ChannelStatsPostInteractions.PostId: ChannelStatsPostInteractions]?, boostData: ChannelBoostStatus?, boostersState: ChannelBoostersContext.State?, giftsState: ChannelBoostersContext.State?, giveawayAvailable: Bool, isGroup: Bool, boostsOnly: Bool, - animatedEmojis: [String: [StickerPackItem]], revenueState: RevenueStats?, revenueTransactions: RevenueStatsTransactionsContext.State, + starsState: StarsRevenueStats?, + starsTransactions: StarsTransactionsContext.State, adsRestricted: Bool, premiumConfiguration: PremiumConfiguration, monetizationConfiguration: MonetizationConfiguration @@ -1512,8 +1711,9 @@ private func channelStatsControllerEntries( data: revenueState, boostData: boostData, transactionsInfo: revenueTransactions, + starsData: starsState, + starsTransactionsInfo: starsTransactions, adsRestricted: adsRestricted, - animatedEmojis: animatedEmojis, premiumConfiguration: premiumConfiguration, monetizationConfiguration: monetizationConfiguration ) @@ -1523,8 +1723,8 @@ private func channelStatsControllerEntries( } public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, section: ChannelStatsSection = .stats, boostStatus: ChannelBoostStatus? = nil, boostStatusUpdated: ((ChannelBoostStatus) -> Void)? = nil) -> ViewController { - let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0), ignoreRepeated: true) - let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0)) + let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, starsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0), ignoreRepeated: true) + let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, starsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0)) let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } @@ -1543,7 +1743,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD let withdrawalDisposable = MetaDisposable() actionsDisposable.add(withdrawalDisposable) - let storiesPromise = Promise() + let storiesPromise = Promise() let statsContext = ChannelStatsContext(postbox: context.account.postbox, network: context.account.network, peerId: peerId) let dataSignal: Signal = statsContext.state @@ -1583,7 +1783,13 @@ public func channelStatsController(context: AccountContext, updatedPresentationD let revenueState = Promise() revenueState.set(.single(nil) |> then(revenueContext.state |> map(Optional.init))) + let starsContext = context.engine.payments.peerStarsRevenueContext(peerId: peerId) + let starsState = Promise() + starsState.set(.single(nil) |> then(starsContext.state |> map(Optional.init))) + let revenueTransactions = RevenueStatsTransactionsContext(account: context.account, peerId: peerId) + let starsTransactions = context.engine.payments.peerStarsTransactionsContext(subject: .peer(peerId), mode: .all) + starsTransactions.loadMore() var dismissAllTooltipsImpl: (() -> Void)? var presentImpl: ((ViewController) -> Void)? @@ -1592,8 +1798,12 @@ public func channelStatsController(context: AccountContext, updatedPresentationD var navigateToChatImpl: ((EnginePeer) -> Void)? var navigateToMessageImpl: ((EngineMessage.Id) -> Void)? var openBoostImpl: ((Bool) -> Void)? - var openTransactionImpl: ((RevenueStatsTransactionsContext.State.Transaction) -> Void)? - var requestWithdrawImpl: (() -> Void)? + var openTonTransactionImpl: ((RevenueStatsTransactionsContext.State.Transaction) -> Void)? + var openStarsTransactionImpl: ((StarsContext.State.Transaction) -> Void)? + var requestTonWithdrawImpl: (() -> Void)? + var requestStarsWithdrawImpl: (() -> Void)? + var showTimeoutTooltipImpl: ((Int32) -> Void)? + var buyAdsImpl: (() -> Void)? var updateStatusBarImpl: ((StatusBarStyle) -> Void)? var dismissInputImpl: (() -> Void)? @@ -1716,8 +1926,20 @@ public func channelStatsController(context: AccountContext, updatedPresentationD updateGiftsSelected: { selected in updateState { $0.withUpdatedGiftsSelected(selected).withUpdatedBoostersExpanded(false) } }, - requestWithdraw: { - requestWithdrawImpl?() + updateStarsSelected: { selected in + updateState { $0.withUpdatedStarsSelected(selected).withUpdatedTransactionsExpanded(false) } + }, + requestTonWithdraw: { + requestTonWithdrawImpl?() + }, + requestStarsWithdraw: { + requestStarsWithdrawImpl?() + }, + showTimeoutTooltip: { timestamp in + showTimeoutTooltipImpl?(timestamp) + }, + buyAds: { + buyAdsImpl?() }, openMonetizationIntro: { let controller = MonetizationIntroScreen(context: context, openMore: {}) @@ -1727,10 +1949,13 @@ public func channelStatsController(context: AccountContext, updatedPresentationD let presentationData = context.sharedContext.currentPresentationData.with { $0 } context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.Monetization_BalanceInfo_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) }, - openTransaction: { transaction in - openTransactionImpl?(transaction) + openTonTransaction: { transaction in + openTonTransactionImpl?(transaction) + }, + openStarsTransaction: { transaction in + openStarsTransactionImpl?(transaction) }, - expandTransactions: { + expandTransactions: { stars in updateState { state in if state.transactionsExpanded { return state.withUpdatedMoreTransactionsDisplayed(state.moreTransactionsDisplayed + 50) @@ -1738,7 +1963,11 @@ public func channelStatsController(context: AccountContext, updatedPresentationD return state.withUpdatedTransactionsExpanded(true) } } - revenueTransactions.loadMore() + if stars { + starsTransactions.loadMore() + } else { + revenueTransactions.loadMore() + } }, updateCpmEnabled: { value in let _ = context.engine.peers.updateChannelRestrictAdMessages(peerId: peerId, restricted: value).start() @@ -1802,12 +2031,13 @@ public func channelStatsController(context: AccountContext, updatedPresentationD giftsContext.state, revenueState.get(), revenueTransactions.state, + starsState.get(), + starsTransactions.state, peerData, - longLoadingSignal, - context.animatedEmojiStickers + longLoadingSignal ) |> deliverOnMainQueue - |> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, revenueState, revenueTransactions, peerData, longLoading, animatedEmojiStickers -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, revenueState, revenueTransactions, starsState, starsTransactions, peerData, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in let (adsRestricted, canViewRevenue) = peerData var isGroup = false @@ -1900,7 +2130,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(presentationData: presentationData, state: state, peer: peer, data: data, messages: messages, stories: stories, interactions: interactions, boostData: boostData, boostersState: boostersState, giftsState: giftsState, giveawayAvailable: premiumConfiguration.giveawayGiftsPurchaseAvailable, isGroup: isGroup, boostsOnly: boostsOnly, animatedEmojis: animatedEmojiStickers, revenueState: revenueState?.stats, revenueTransactions: revenueTransactions, adsRestricted: adsRestricted, premiumConfiguration: premiumConfiguration, monetizationConfiguration: monetizationConfiguration), style: .blocks, emptyStateItem: emptyStateItem, headerItem: headerItem, crossfadeState: previous == nil, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(presentationData: presentationData, state: state, peer: peer, data: data, messages: messages, stories: stories, interactions: interactions, boostData: boostData, boostersState: boostersState, giftsState: giftsState, giveawayAvailable: premiumConfiguration.giveawayGiftsPurchaseAvailable, isGroup: isGroup, boostsOnly: boostsOnly, revenueState: revenueState?.stats, revenueTransactions: revenueTransactions, starsState: starsState?.stats, starsTransactions: starsTransactions, adsRestricted: adsRestricted, premiumConfiguration: premiumConfiguration, monetizationConfiguration: monetizationConfiguration), style: .blocks, emptyStateItem: emptyStateItem, headerItem: headerItem, crossfadeState: previous == nil, animateChanges: false) return (controllerState, (listState, arguments)) } @@ -1909,6 +2139,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD let _ = statsContext.state let _ = storyList.state let _ = revenueContext.state + let _ = starsContext.state } let controller = ItemListController(context: context, state: signal) @@ -2122,7 +2353,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD }) } } - requestWithdrawImpl = { + requestTonWithdrawImpl = { withdrawalDisposable.set((context.engine.peers.checkChannelRevenueWithdrawalAvailability() |> deliverOnMainQueue).start(error: { error in let controller = revenueWithdrawalController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, initialError: error, present: { c, _ in @@ -2134,7 +2365,102 @@ public func channelStatsController(context: AccountContext, updatedPresentationD presentImpl?(controller) })) } - openTransactionImpl = { transaction in + requestStarsWithdrawImpl = { + withdrawalDisposable.set((context.engine.peers.checkStarsRevenueWithdrawalAvailability() + |> deliverOnMainQueue).start(error: { error in + switch error { + case .serverProvided: + return + case .requestPassword: + let _ = (starsContext.state + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { state in + guard let stats = state.stats else { + return + } + let controller = context.sharedContext.makeStarsWithdrawalScreen(context: context, stats: stats, completion: { amount in + let controller = confirmStarsRevenueWithdrawalController(context: context, peerId: peerId, amount: amount, present: { c, a in + presentImpl?(c) + }, completion: { url in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + + Queue.mainQueue().after(2.0) { + starsContext.reload() + starsTransactions.reload() + } + }) + presentImpl?(controller) + }) + pushImpl?(controller) + }) + default: + let controller = starsRevenueWithdrawalController(context: context, peerId: peerId, amount: 0, initialError: error, present: { c, a in + presentImpl?(c) + }, completion: { _ in + + }) + presentImpl?(controller) + } + })) + } + var tooltipScreen: UndoOverlayController? + var timer: Foundation.Timer? + showTimeoutTooltipImpl = { cooldownUntilTimestamp in + let remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let content: UndoOverlayContent = .universal( + animation: "anim_clock", + scale: 0.058, + colors: [:], + title: nil, + text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorTimeout(stringForRemainingTime(remainingCooldownSeconds)).string, + customUndoText: nil, + timeout: nil + ) + let controller = UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in + return true + }) + tooltipScreen = controller + presentImpl?(controller) + + if remainingCooldownSeconds < 3600 { + if timer == nil { + timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { _ in + if let tooltipScreen { + let remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) + let content: UndoOverlayContent = .universal( + animation: "anim_clock", + scale: 0.058, + colors: [:], + title: nil, + text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorTimeout(stringForRemainingTime(remainingCooldownSeconds)).string, + customUndoText: nil, + timeout: nil + ) + tooltipScreen.content = content + } else { + if let currentTimer = timer { + timer = nil + currentTimer.invalidate() + } + } + }) + } + } + } + buyAdsImpl = { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let _ = (context.engine.peers.requestStarsRevenueAdsAccountlUrl(peerId: peerId) + |> deliverOnMainQueue).startStandalone(next: { url in + guard let url else { + return + } + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + }) + } + openTonTransactionImpl = { transaction in let _ = (peer.get() |> take(1) |> deliverOnMainQueue).start(next: { peer in @@ -2146,6 +2472,16 @@ public func channelStatsController(context: AccountContext, updatedPresentationD })) }) } + openStarsTransactionImpl = { transaction in + let _ = (peer.get() + |> take(1) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + pushImpl?(context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction, peer: peer)) + }) + } updateStatusBarImpl = { [weak controller] style in controller?.setStatusBarStyle(style, animated: true) } diff --git a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift index 2e7ad59bdc7..b63f1e4c79f 100644 --- a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift +++ b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift @@ -9,24 +9,31 @@ import ItemListUI import SolidRoundedButtonNode import TelegramCore import TextFormat +import ComponentFlow +import ButtonComponent +import BundleIconComponent final class MonetizationBalanceItem: ListViewItem, ItemListItem { let context: AccountContext let presentationData: ItemListPresentationData - let stats: RevenueStats + let stats: Stats let canWithdraw: Bool let isEnabled: Bool + let actionCooldownUntilTimestamp: Int32? let withdrawAction: () -> Void + let buyAdsAction: (() -> Void)? let sectionId: ItemListSectionId let style: ItemListStyle init( context: AccountContext, presentationData: ItemListPresentationData, - stats: RevenueStats, + stats: Stats, canWithdraw: Bool, isEnabled: Bool, + actionCooldownUntilTimestamp: Int32?, withdrawAction: @escaping () -> Void, + buyAdsAction: (() -> Void)?, sectionId: ItemListSectionId, style: ItemListStyle ) { @@ -35,7 +42,9 @@ final class MonetizationBalanceItem: ListViewItem, ItemListItem { self.stats = stats self.canWithdraw = canWithdraw self.isEnabled = isEnabled + self.actionCooldownUntilTimestamp = actionCooldownUntilTimestamp self.withdrawAction = withdrawAction + self.buyAdsAction = buyAdsAction self.sectionId = sectionId self.style = style } @@ -85,12 +94,15 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode { private let iconNode: ASImageNode private let balanceTextNode: TextNode private let valueTextNode: TextNode - - private var withdrawButtonNode: SolidRoundedButtonNode? + private var button = ComponentView() + private var buyButton = ComponentView() private let activateArea: AccessibilityAreaNode + private var timer: Foundation.Timer? + private var item: MonetizationBalanceItem? + private var buttonLayout: (isStars: Bool, origin: CGFloat, width: CGFloat, leftInset: CGFloat, rightInset: CGFloat)? override var canBeSelected: Bool { return false @@ -158,13 +170,24 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode { let integralFont = Font.with(size: 48.0, design: .round, weight: .semibold) let fractionalFont = Font.with(size: 24.0, design: .round, weight: .semibold) - let cryptoValue = formatBalanceText(item.stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) + let amountString: NSAttributedString + let value: String - let amountString = amountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor) + var isStars = false + if let stats = item.stats as? RevenueStats { + let cryptoValue = formatBalanceText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) + amountString = amountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor) + value = stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))" + } else if let stats = item.stats as? StarsRevenueStats { + amountString = NSAttributedString(string: presentationStringsFormattedNumber(Int32(stats.balances.availableBalance), item.presentationData.dateTimeFormat.groupingSeparator), font: integralFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + value = stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.availableBalance, divide: false, rate: stats.usdRate))" + isStars = true + } else { + fatalError() + } let (balanceLayout, balanceApply) = makeBalanceTextLayout(TextNodeLayoutArguments(attributedString: amountString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - let value = item.stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(item.stats.balances.availableBalance, rate: item.stats.usdRate))" let (valueLayout, valueApply) = makeValueTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: value, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let verticalInset: CGFloat = 13.0 @@ -272,7 +295,11 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode { } if themeUpdated { - strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonBig"), color: item.presentationData.theme.list.itemAccentColor) + if isStars { + strongSelf.iconNode.image = UIImage(bundleImageName: "Premium/Stars/BalanceStar") + } else { + strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonBig"), color: item.presentationData.theme.list.itemAccentColor) + } } var emojiItemSize = CGSize() @@ -288,34 +315,128 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode { strongSelf.valueTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - valueLayout.size.width) / 2.0), y: balanceTextFrame.maxY - 5.0), size: valueLayout.size) - if item.canWithdraw { - let withdrawButtonNode: SolidRoundedButtonNode - if let currentWithdrawButtonNode = strongSelf.withdrawButtonNode { - withdrawButtonNode = currentWithdrawButtonNode - } else { - var buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme) - buttonTheme = buttonTheme.withUpdated(disabledBackgroundColor: buttonTheme.backgroundColor, disabledForegroundColor: buttonTheme.foregroundColor.withAlphaComponent(0.6)) - withdrawButtonNode = SolidRoundedButtonNode(theme: buttonTheme, height: buttonHeight, cornerRadius: 11.0) - withdrawButtonNode.pressed = { [weak self] in - if let self, let item = self.item, item.isEnabled { - item.withdrawAction() - } - } - strongSelf.addSubnode(withdrawButtonNode) - strongSelf.withdrawButtonNode = withdrawButtonNode + strongSelf.buttonLayout = (isStars: isStars, origin: strongSelf.valueTextNode.frame.maxY + buttonSpacing + 3.0, width: params.width, leftInset: leftInset, rightInset: rightInset) + strongSelf.updateButton() + } + }) + } + } + + func updateButton() { + guard let item = self.item, let (isStars, origin, width, leftInset, rightInset) = self.buttonLayout else { + return + } + + if item.canWithdraw { + var remainingCooldownSeconds: Int32 = 0 + if let cooldownUntilTimestamp = item.actionCooldownUntilTimestamp { + remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) + remainingCooldownSeconds = max(0, remainingCooldownSeconds) + } + + if remainingCooldownSeconds > 0 { + if self.timer == nil { + self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in + guard let self else { + return } - withdrawButtonNode.title = item.presentationData.strings.Monetization_BalanceWithdraw - withdrawButtonNode.isEnabled = item.isEnabled + self.updateButton() + }) + } + } else { + if let timer = self.timer { + self.timer = nil + timer.invalidate() + } + } + + var actionTitle = isStars ? item.presentationData.strings.Monetization_BalanceStarsWithdraw : item.presentationData.strings.Monetization_BalanceWithdraw + var withdrawWidth = width - leftInset - rightInset + if let _ = item.buyAdsAction { + withdrawWidth = (withdrawWidth - 10.0) / 2.0 + actionTitle = item.presentationData.strings.Monetization_BalanceStarsWithdrawShort + } + + let content: AnyComponentWithIdentity + if remainingCooldownSeconds > 0 { + content = AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent( + VStack([ + AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(Text(text: actionTitle, font: Font.semibold(17.0), color: item.presentationData.theme.list.itemCheckColors.foregroundColor))), + AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(HStack([ + AnyComponentWithIdentity(id: 1, component: AnyComponent(BundleIconComponent(name: "Chat List/StatusLockIcon", tintColor: item.presentationData.theme.list.itemCheckColors.fillColor.mixedWith(item.presentationData.theme.list.itemCheckColors.foregroundColor, alpha: 0.7)))), + AnyComponentWithIdentity(id: 0, component: AnyComponent(Text(text: stringForRemainingTime(remainingCooldownSeconds), font: Font.with(size: 11.0, weight: .medium, traits: [.monospacedNumbers]), color: item.presentationData.theme.list.itemCheckColors.fillColor.mixedWith(item.presentationData.theme.list.itemCheckColors.foregroundColor, alpha: 0.7)))) + ], spacing: 3.0))) + ], spacing: 1.0) + )) + } else { + content = AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: actionTitle, font: Font.semibold(17.0), color: item.presentationData.theme.list.itemCheckColors.foregroundColor))) + } - let buttonWidth = contentSize.width - leftInset - rightInset - let _ = withdrawButtonNode.updateLayout(width: buttonWidth, transition: .immediate) - withdrawButtonNode.frame = CGRect(x: leftInset, y: strongSelf.valueTextNode.frame.maxY + buttonSpacing + 3.0, width: buttonWidth, height: buttonHeight) - } else { - strongSelf.withdrawButtonNode?.removeFromSupernode() - strongSelf.withdrawButtonNode = nil + let buttonSize = self.button.update( + transition: .immediate, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: item.presentationData.theme.list.itemCheckColors.fillColor, + foreground: item.presentationData.theme.list.itemCheckColors.foregroundColor, + pressedColor: item.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: content, + isEnabled: item.isEnabled, + allowActionWhenDisabled: false, + displaysProgress: false, + action: { [weak self] in + guard let self, let item = self.item, item.isEnabled else { + return + } + item.withdrawAction() } + )), + environment: {}, + containerSize: CGSize(width: withdrawWidth, height: 50.0) + ) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.view.addSubview(buttonView) } - }) + let buttonFrame = CGRect(origin: CGPoint(x: leftInset, y: origin), size: buttonSize) + buttonView.frame = buttonFrame + } + + if let _ = item.buyAdsAction { + let buyButtonSize = self.buyButton.update( + transition: .immediate, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: item.presentationData.theme.list.itemCheckColors.fillColor, + foreground: item.presentationData.theme.list.itemCheckColors.foregroundColor, + pressedColor: item.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: item.presentationData.strings.Monetization_BalanceStarsBuyAds, font: Font.semibold(17.0), color: item.presentationData.theme.list.itemCheckColors.foregroundColor))), + isEnabled: true, + allowActionWhenDisabled: false, + displaysProgress: false, + action: { [weak self] in + guard let self, let item = self.item, item.isEnabled else { + return + } + item.buyAdsAction?() + } + )), + environment: {}, + containerSize: CGSize(width: withdrawWidth, height: 50.0) + ) + if let buttonView = self.buyButton.view { + if buttonView.superview == nil { + self.view.addSubview(buttonView) + } + let buttonFrame = CGRect(origin: CGPoint(x: leftInset + withdrawWidth + 10.0, y: origin), size: buyButtonSize) + buttonView.frame = buttonFrame + } + } else if let buttonView = self.buyButton.view { + buttonView.removeFromSuperview() + } + } else if let buttonView = self.button.view { + buttonView.removeFromSuperview() } } @@ -331,3 +452,16 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } } + +func stringForRemainingTime(_ duration: Int32) -> String { + let hours = duration / 3600 + let minutes = duration / 60 % 60 + let seconds = duration % 60 + let durationString: String + if hours > 0 { + durationString = String(format: "%d:%02d", hours, minutes) + } else { + durationString = String(format: "%02d:%02d", minutes, seconds) + } + return durationString +} diff --git a/submodules/StatisticsUI/Sources/MonetizationUtils.swift b/submodules/StatisticsUI/Sources/MonetizationUtils.swift index 8c886275789..3635540ff72 100644 --- a/submodules/StatisticsUI/Sources/MonetizationUtils.swift +++ b/submodules/StatisticsUI/Sources/MonetizationUtils.swift @@ -9,8 +9,9 @@ func formatAddress(_ address: String) -> String { return address } -func formatUsdValue(_ value: Int64, rate: Double) -> String { - let formattedValue = String(format: "%0.2f", (Double(value) / 1000000000) * rate) +func formatUsdValue(_ value: Int64, divide: Bool = true, rate: Double) -> String { + let normalizedValue: Double = divide ? Double(value) / 1000000000 : Double(value) + let formattedValue = String(format: "%0.2f", normalizedValue * rate) return "$\(formattedValue)" } @@ -38,6 +39,11 @@ func formatBalanceText(_ value: Int64, decimalSeparator: String, showPlus: Bool } else if showPlus { balanceText.insert("+", at: balanceText.startIndex) } + + if let dec = balanceText.range(of: decimalSeparator) { + balanceText = String(balanceText[balanceText.startIndex ..< min(balanceText.endIndex, balanceText.index(dec.upperBound, offsetBy: 2))]) + } + return balanceText } diff --git a/submodules/StatisticsUI/Sources/RevenueWithdrawalController.swift b/submodules/StatisticsUI/Sources/RevenueWithdrawalController.swift index 77755aa3ec2..7664d1e2cd2 100644 --- a/submodules/StatisticsUI/Sources/RevenueWithdrawalController.swift +++ b/submodules/StatisticsUI/Sources/RevenueWithdrawalController.swift @@ -4,10 +4,10 @@ import SwiftSignalKit import TelegramCore import TelegramPresentationData import PresentationDataUtils -import PeerInfoUI import AccountContext import PasswordSetupUI import Markdown +import OwnershipTransferController func confirmRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift new file mode 100644 index 00000000000..6a225c18dcd --- /dev/null +++ b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift @@ -0,0 +1,397 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import AccountContext +import TelegramPresentationData +import ItemListUI +import ComponentFlow +import ListActionItemComponent +import MultilineTextComponent +import TelegramStringFormatting +import StarsAvatarComponent + +final class StarsTransactionItem: ListViewItem, ItemListItem { + let context: AccountContext + let presentationData: ItemListPresentationData + let transaction: StarsContext.State.Transaction + let action: () -> Void + let sectionId: ItemListSectionId + let style: ItemListStyle + + init( + context: AccountContext, + presentationData: ItemListPresentationData, + transaction: StarsContext.State.Transaction, + action: @escaping () -> Void, + sectionId: ItemListSectionId, + style: ItemListStyle + ) { + self.context = context + self.presentationData = presentationData + self.transaction = transaction + self.action = action + self.sectionId = sectionId + self.style = style + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = StarsTransactionItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + 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 { + if let nodeValue = node() as? StarsTransactionItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } + + var selectable: Bool = true + + public func selected(listView: ListView) { + listView.clearHighlightAnimated(true) + self.action() + } +} + +final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let maskNode: ASImageNode + + private let componentView: ComponentView + + private let activateArea: AccessibilityAreaNode + + private var item: StarsTransactionItem? + + var tag: ItemListItemTag? = nil + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.maskNode = ASImageNode() + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + self.componentView = ComponentView() + + self.activateArea = AccessibilityAreaNode() + + super.init(layerBacked: false, dynamicBounce: false) + } + + func asyncLayout() -> (_ item: StarsTransactionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + + return { item, params, neighbors in + var updatedTheme: PresentationTheme? + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + + let leftInset = 16.0 + params.leftInset + + let height: CGFloat = 78.0 + + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = .clear + insets = UIEdgeInsets() + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + insets = itemListNeighborsGroupedInsets(neighbors, params) + } + + contentSize = CGSize(width: params.width, height: height) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + strongSelf.activateArea.accessibilityTraits = [] + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + } + + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + if strongSelf.maskNode.supernode != nil { + strongSelf.maskNode.removeFromSupernode() + } + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) + } + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)) + + let fontBaseDisplaySize = 17.0 + + let itemTitle: String + let itemSubtitle: String? + var itemDate: String + switch item.transaction.peer { + case let .peer(peer): + if !item.transaction.media.isEmpty { + itemTitle = item.presentationData.strings.Stars_Intro_Transaction_MediaPurchase + itemSubtitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast) + } else if let title = item.transaction.title { + itemTitle = title + itemSubtitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast) + } else { + itemTitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast) + itemSubtitle = nil + } + case .appStore: + itemTitle = item.presentationData.strings.Stars_Intro_Transaction_AppleTopUp_Title + itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_AppleTopUp_Subtitle + case .playMarket: + itemTitle = item.presentationData.strings.Stars_Intro_Transaction_GoogleTopUp_Title + itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_GoogleTopUp_Subtitle + case .fragment: + itemTitle = item.presentationData.strings.Stars_Intro_Transaction_FragmentWithdrawal_Title + itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_FragmentWithdrawal_Subtitle + case .premiumBot: + itemTitle = item.presentationData.strings.Stars_Intro_Transaction_PremiumBotTopUp_Title + itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_PremiumBotTopUp_Subtitle + case .ads: + itemTitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramAds_Title + itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramAds_Subtitle + case .unsupported: + itemTitle = item.presentationData.strings.Stars_Intro_Transaction_Unsupported_Title + itemSubtitle = nil + } + + let itemLabel: NSAttributedString + let labelString: String + + let formattedLabel = presentationStringsFormattedNumber(abs(Int32(item.transaction.count)), item.presentationData.dateTimeFormat.groupingSeparator) + if item.transaction.count < 0 { + labelString = "- \(formattedLabel)" + } else { + labelString = "+ \(formattedLabel)" + } + itemLabel = NSAttributedString(string: labelString, font: Font.medium(fontBaseDisplaySize), textColor: labelString.hasPrefix("-") ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor) + + itemDate = stringForMediumCompactDate(timestamp: item.transaction.date, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat) + if item.transaction.flags.contains(.isRefund) { + itemDate += " – \(item.presentationData.strings.Stars_Intro_Transaction_Refund)" + } + + var titleComponents: [AnyComponentWithIdentity] = [] + titleComponents.append( + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: itemTitle, + font: Font.semibold(fontBaseDisplaySize), + textColor: item.presentationData.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))) + ) + if let itemSubtitle { + titleComponents.append( + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: itemSubtitle, + font: Font.regular(fontBaseDisplaySize * 16.0 / 17.0), + textColor: item.presentationData.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))) + ) + } + titleComponents.append( + AnyComponentWithIdentity(id: AnyHashable(2), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: itemDate, + font: Font.regular(floor(fontBaseDisplaySize * 14.0 / 17.0)), + textColor: item.presentationData.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + ))) + ) + let itemSize = strongSelf.componentView.update( + transition: .immediate, + component: AnyComponent(ListActionItemComponent( + theme: item.presentationData.theme, + title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)), + contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0), + leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: item.context, theme: item.presentationData.theme, peer: item.transaction.peer, photo: nil, media: [], backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor))), false), + icon: nil, + accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), + action: { [weak self] _ in + guard let self, let item = self.item else { + return + } + if !item.transaction.flags.contains(.isLocal) { + item.action() + } + } + )), + environment: {}, + containerSize: CGSize(width: params.width - params.leftInset - params.rightInset, height: height) + ) + let itemFrame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: itemSize) + if let itemComponentView = strongSelf.componentView.view { + if itemComponentView.superview == nil { + strongSelf.view.addSubview(itemComponentView) + } + itemComponentView.isUserInteractionEnabled = false + itemComponentView.frame = itemFrame + } + } + }) + } + } + + + override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/StatisticsUI/Sources/StatsGraphItem.swift b/submodules/StatisticsUI/Sources/StatsGraphItem.swift index 4a618efcb90..c462f1bd27e 100644 --- a/submodules/StatisticsUI/Sources/StatsGraphItem.swift +++ b/submodules/StatisticsUI/Sources/StatsGraphItem.swift @@ -10,18 +10,19 @@ import PresentationDataUtils import GraphCore import GraphUI import ActivityIndicator +import ListItemComponentAdaptor -class StatsGraphItem: ListViewItem, ItemListItem { +public final class StatsGraphItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { let presentationData: ItemListPresentationData let graph: StatsGraph let type: ChartType let noInitialZoom: Bool let conversionRate: Double let getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)? - let sectionId: ItemListSectionId + public let sectionId: ItemListSectionId let style: ItemListStyle - init(presentationData: ItemListPresentationData, graph: StatsGraph, type: ChartType, noInitialZoom: Bool = false, conversionRate: Double = 1.0, getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)? = nil, sectionId: ItemListSectionId, style: ItemListStyle) { + public init(presentationData: ItemListPresentationData, graph: StatsGraph, type: ChartType, noInitialZoom: Bool = false, conversionRate: Double = 1.0, getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)? = nil, sectionId: ItemListSectionId, style: ItemListStyle) { self.presentationData = presentationData self.graph = graph self.type = type @@ -32,7 +33,7 @@ class StatsGraphItem: ListViewItem, ItemListItem { self.style = style } - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = StatsGraphItemNode() let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) @@ -48,7 +49,7 @@ class StatsGraphItem: ListViewItem, ItemListItem { } } - 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) { + 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 { if let nodeValue = node() as? StatsGraphItemNode { let makeLayout = nodeValue.asyncLayout() @@ -65,10 +66,39 @@ class StatsGraphItem: ListViewItem, ItemListItem { } } - var selectable: Bool = false + public func item() -> ListViewItem { + return self + } + + public static func ==(lhs: StatsGraphItem, rhs: StatsGraphItem) -> Bool { + if lhs.presentationData !== rhs.presentationData { + return false + } + if lhs.graph != rhs.graph { + return false + } + if lhs.type != rhs.type { + return false + } + if lhs.noInitialZoom != rhs.noInitialZoom { + return false + } + if lhs.conversionRate != rhs.conversionRate { + return false + } + if lhs.sectionId != rhs.sectionId { + return false + } + if lhs.style != rhs.style { + return false + } + return true + } + + public var selectable: Bool = false } -class StatsGraphItemNode: ListViewItemNode { +public final class StatsGraphItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode @@ -109,7 +139,7 @@ class StatsGraphItemNode: ListViewItemNode { self.chartContainerNode.addSubnode(self.activityIndicator) } - override func didLoad() { + public override func didLoad() { super.didLoad() self.view.interactiveTransitionGestureRecognizerTest = { point -> Bool in @@ -117,7 +147,7 @@ class StatsGraphItemNode: ListViewItemNode { } } - func resetInteraction() { + public func resetInteraction() { self.chartNode.resetInteraction() } @@ -283,15 +313,15 @@ class StatsGraphItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + public override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + public override func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + public override func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } } diff --git a/submodules/StatisticsUI/Sources/StatsMessageItem.swift b/submodules/StatisticsUI/Sources/StatsMessageItem.swift index 64323ad58d9..564c4c2d06a 100644 --- a/submodules/StatisticsUI/Sources/StatsMessageItem.swift +++ b/submodules/StatisticsUI/Sources/StatsMessageItem.swift @@ -649,7 +649,7 @@ final class StatsMessageItemNode: ListViewItemNode, ItemListItemNode { let indicatorSize = CGSize(width: imageSize.width - lineWidth * 4.0, height: imageSize.height - lineWidth * 4.0) let storyIndicator: ComponentView - let indicatorTransition: Transition = .immediate + let indicatorTransition: ComponentTransition = .immediate if let current = strongSelf.storyIndicator { storyIndicator = current } else { diff --git a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift index 80510f2a7d2..8e72379fdf8 100644 --- a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift +++ b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift @@ -39,25 +39,29 @@ extension RevenueStats: Stats { } +extension StarsRevenueStats: Stats { + +} + class StatsOverviewItem: ListViewItem, ItemListItem { let context: AccountContext let presentationData: ItemListPresentationData let isGroup: Bool let stats: Stats + let additionalStats: Stats? let storyViews: EngineStoryItem.Views? let publicShares: Int32? - let animatedEmoji: TelegramMediaFile? let sectionId: ItemListSectionId let style: ItemListStyle - init(context: AccountContext, presentationData: ItemListPresentationData, isGroup: Bool, stats: Stats, storyViews: EngineStoryItem.Views? = nil, publicShares: Int32? = nil, animatedEmoji: TelegramMediaFile? = nil, sectionId: ItemListSectionId, style: ItemListStyle) { + init(context: AccountContext, presentationData: ItemListPresentationData, isGroup: Bool, stats: Stats, additionalStats: Stats? = nil, storyViews: EngineStoryItem.Views? = nil, publicShares: Int32? = nil, sectionId: ItemListSectionId, style: ItemListStyle) { self.context = context self.presentationData = presentationData self.isGroup = isGroup self.stats = stats + self.additionalStats = additionalStats self.storyViews = storyViews self.publicShares = publicShares - self.animatedEmoji = animatedEmoji self.sectionId = sectionId self.style = style } @@ -99,6 +103,12 @@ class StatsOverviewItem: ListViewItem, ItemListItem { } private final class ValueItemNode: ASDisplayNode { + enum Mode { + case generic + case ton + case stars + } + enum DeltaColor { case generic case positive @@ -110,6 +120,7 @@ private final class ValueItemNode: ASDisplayNode { private let deltaNode: TextNode private var iconNode: ASImageNode? + var currentIconName: String? var currentTheme: PresentationTheme? var pressed: (() -> Void)? @@ -127,13 +138,13 @@ private final class ValueItemNode: ASDisplayNode { self.addSubnode(self.deltaNode) } - static func asyncLayout(_ current: ValueItemNode?) -> (_ context: AccountContext, _ width: CGFloat, _ presentationData: ItemListPresentationData, _ value: String, _ title: String, _ delta: (String, DeltaColor)?, _ isTon: Bool) -> (CGSize, () -> ValueItemNode) { + static func asyncLayout(_ current: ValueItemNode?) -> (_ context: AccountContext, _ width: CGFloat, _ presentationData: ItemListPresentationData, _ value: String, _ title: String, _ delta: (String, DeltaColor)?, _ mode: Mode) -> (CGSize, () -> ValueItemNode) { let maybeMakeValueLayout = (current?.valueNode).flatMap(TextNode.asyncLayout) let maybeMakeTitleLayout = (current?.titleNode).flatMap(TextNode.asyncLayout) let maybeMakeDeltaLayout = (current?.deltaNode).flatMap(TextNode.asyncLayout) - return { context, width, presentationData, value, title, delta, isTon in + return { context, width, presentationData, value, title, delta, mode in let targetNode: ValueItemNode if let current = current { targetNode = current @@ -188,7 +199,7 @@ private final class ValueItemNode: ASDisplayNode { let constrainedSize = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude) let valueString: NSAttributedString - if isTon { + if case .ton = mode { valueString = amountAttributedString(value, integralFont: valueFont, fractionalFont: smallValueFont, color: valueColor) } else { valueString = NSAttributedString(string: value, font: valueFont, textColor: valueColor) @@ -200,8 +211,26 @@ private final class ValueItemNode: ASDisplayNode { let (deltaLayout, deltaApply) = makeDeltaLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: delta?.0 ?? "", font: deltaFont, textColor: deltaColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + var valueOffset: CGFloat = 0.0 + let iconName: String? + var iconTinted = false + switch mode { + case .ton: + iconName = "Ads/TonMedium" + iconTinted = true + valueOffset = 17.0 + case .stars: + iconName = "Premium/Stars/StarMedium" + valueOffset = 21.0 + default: + iconName = nil + } + let horizontalSpacing: CGFloat = 4.0 - let size = CGSize(width: valueLayout.size.width + horizontalSpacing + deltaLayout.size.width, height: valueLayout.size.height + titleLayout.size.height) + let size = CGSize( + width: max(valueOffset + valueLayout.size.width + horizontalSpacing + deltaLayout.size.width, titleLayout.size.width), + height: valueLayout.size.height + titleLayout.size.height + ) return (size, { var themeUpdated = false if targetNode.currentTheme !== presentationData.theme { @@ -212,9 +241,14 @@ private final class ValueItemNode: ASDisplayNode { let _ = valueApply() let _ = titleApply() let _ = deltaApply() + + var iconNameUpdated = false + if targetNode.currentIconName != iconName { + targetNode.currentIconName = iconName + iconNameUpdated = true + } - var valueOffset: CGFloat = 0.0 - if isTon { + if let iconName { let iconNode: ASImageNode if let current = targetNode.iconNode { iconNode = current @@ -225,16 +259,18 @@ private final class ValueItemNode: ASDisplayNode { targetNode.addSubnode(iconNode) } - if themeUpdated { - iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonMedium"), color: presentationData.theme.list.itemAccentColor) + if themeUpdated || iconNameUpdated { + if iconTinted { + iconNode.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: presentationData.theme.list.itemAccentColor) + } else { + iconNode.image = UIImage(bundleImageName: iconName) + } } if let icon = iconNode.image { let iconFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((valueLayout.size.height - icon.size.height) / 2.0) - 1.0), size: icon.size) iconNode.frame = iconFrame } - - valueOffset += 17.0 } else if let iconNode = targetNode.iconNode { iconNode.removeFromSupernode() targetNode.iconNode = nil @@ -352,6 +388,7 @@ class StatsOverviewItemNode: ListViewItemNode { } var twoColumnLayout = true + var useMinLeftColumnWidth = true var topLeftItemLayoutAndApply: (CGSize, () -> ValueItemNode)? var topRightItemLayoutAndApply: (CGSize, () -> ValueItemNode)? @@ -382,7 +419,7 @@ class StatsOverviewItemNode: ListViewItemNode { compactNumericCountString(stats.views), item.presentationData.strings.Stats_Message_Views, nil, - false + .generic ) topRightItemLayoutAndApply = makeTopRightItemLayout( @@ -392,7 +429,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–", item.presentationData.strings.Stats_Message_PublicShares, nil, - false + .generic ) middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout( @@ -402,7 +439,7 @@ class StatsOverviewItemNode: ListViewItemNode { compactNumericCountString(stats.reactions), item.presentationData.strings.Stats_Message_Reactions, nil, - false + .generic ) middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout( @@ -412,7 +449,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, stats.forwards - Int($0))))" } ?? "–", item.presentationData.strings.Stats_Message_PrivateShares, nil, - false + .generic ) height += topRightItemLayoutAndApply!.0.height * 2.0 + verticalSpacing @@ -424,7 +461,7 @@ class StatsOverviewItemNode: ListViewItemNode { compactNumericCountString(views.seenCount), item.presentationData.strings.Stats_Message_Views, nil, - false + .generic ) topRightItemLayoutAndApply = makeTopRightItemLayout( @@ -434,7 +471,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–", item.presentationData.strings.Stats_Message_PublicShares, nil, - false + .generic ) middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout( @@ -444,7 +481,7 @@ class StatsOverviewItemNode: ListViewItemNode { compactNumericCountString(views.reactedCount), item.presentationData.strings.Stats_Message_Reactions, nil, - false + .generic ) middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout( @@ -454,7 +491,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, views.forwardCount - Int($0))))" } ?? "–", item.presentationData.strings.Stats_Message_PrivateShares, nil, - false + .generic ) height += topRightItemLayoutAndApply!.0.height * 2.0 + verticalSpacing @@ -466,7 +503,7 @@ class StatsOverviewItemNode: ListViewItemNode { "\(stats.level)", item.presentationData.strings.Stats_Boosts_Level, nil, - false + .generic ) var premiumSubscribers: Double = 0.0 @@ -481,7 +518,7 @@ class StatsOverviewItemNode: ListViewItemNode { "≈\(Int(stats.premiumAudience?.value ?? 0))", item.isGroup ? item.presentationData.strings.Stats_Boosts_PremiumMembers : item.presentationData.strings.Stats_Boosts_PremiumSubscribers, (String(format: "%.02f%%", premiumSubscribers * 100.0), .generic), - false + .generic ) middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout( @@ -491,7 +528,7 @@ class StatsOverviewItemNode: ListViewItemNode { "\(stats.boosts)", item.presentationData.strings.Stats_Boosts_ExistingBoosts, nil, - false + .generic ) let boostsLeft: Int32 @@ -507,7 +544,7 @@ class StatsOverviewItemNode: ListViewItemNode { "\(boostsLeft)", item.presentationData.strings.Stats_Boosts_BoostsToLevelUp, nil, - false + .generic ) if twoColumnLayout { @@ -532,7 +569,7 @@ class StatsOverviewItemNode: ListViewItemNode { compactNumericCountString(Int(stats.followers.current)), item.presentationData.strings.Stats_Followers, (followersDelta.text, followersDelta.positive ? .positive : .negative), - false + .generic ) var enabledNotifications: Double = 0.0 @@ -546,7 +583,7 @@ class StatsOverviewItemNode: ListViewItemNode { String(format: "%.02f%%", enabledNotifications * 100.0), item.presentationData.strings.Stats_EnabledNotifications, nil, - false + .generic ) let hasMessages = stats.viewsPerPost.current > 0 || viewsPerPostDelta.hasValue @@ -605,7 +642,7 @@ class StatsOverviewItemNode: ListViewItemNode { value, title, delta, - false + .generic ) } if let (value, title, delta) = items[1] { @@ -616,7 +653,7 @@ class StatsOverviewItemNode: ListViewItemNode { value, title, delta, - false + .generic ) } if let (value, title, delta) = items[2] { @@ -627,7 +664,7 @@ class StatsOverviewItemNode: ListViewItemNode { value, title, delta, - false + .generic ) } if let (value, title, delta) = items[3] { @@ -638,7 +675,7 @@ class StatsOverviewItemNode: ListViewItemNode { value, title, delta, - false + .generic ) } if let (value, title, delta) = items[4] { @@ -649,7 +686,7 @@ class StatsOverviewItemNode: ListViewItemNode { value, title, delta, - false + .generic ) } if let (value, title, delta) = items[5] { @@ -660,7 +697,7 @@ class StatsOverviewItemNode: ListViewItemNode { value, title, delta, - false + .generic ) } @@ -684,7 +721,7 @@ class StatsOverviewItemNode: ListViewItemNode { compactNumericCountString(Int(stats.members.current)), item.presentationData.strings.Stats_GroupMembers, (membersDelta.text, membersDelta.positive ? .positive : .negative), - false + .generic ) let messagesDelta = deltaText(stats.messages) @@ -695,7 +732,7 @@ class StatsOverviewItemNode: ListViewItemNode { compactNumericCountString(Int(stats.messages.current)), item.presentationData.strings.Stats_GroupMessages, (messagesDelta.text, messagesDelta.positive ? .positive : .negative), - false + .generic ) if displayBottomRow { @@ -706,7 +743,7 @@ class StatsOverviewItemNode: ListViewItemNode { compactNumericCountString(Int(stats.viewers.current)), item.presentationData.strings.Stats_GroupViewers, (viewersDelta.text, viewersDelta.positive ? .positive : .negative), - false + .generic ) middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout( @@ -716,7 +753,7 @@ class StatsOverviewItemNode: ListViewItemNode { compactNumericCountString(Int(stats.posters.current)), item.presentationData.strings.Stats_GroupPosters, (postersDelta.text, postersDelta.positive ? .positive : .negative), - false + .generic ) } @@ -726,39 +763,106 @@ class StatsOverviewItemNode: ListViewItemNode { height += topLeftItemLayoutAndApply!.0.height * 4.0 + verticalSpacing * 3.0 } } else if let stats = item.stats as? RevenueStats { - twoColumnLayout = false - - topLeftItemLayoutAndApply = makeTopLeftItemLayout( - item.context, - params.width, - item.presentationData, - formatBalanceText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), - item.presentationData.strings.Monetization_Overview_Available, - (stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))", .generic), - true - ) - - topRightItemLayoutAndApply = makeTopRightItemLayout( - item.context, - params.width, - item.presentationData, - formatBalanceText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), - item.presentationData.strings.Monetization_Overview_Current, - (stats.balances.currentBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.currentBalance, rate: stats.usdRate))", .generic), - true - ) - - middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout( - item.context, - params.width, - item.presentationData, - formatBalanceText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), - item.presentationData.strings.Monetization_Overview_Total, - (stats.balances.overallRevenue == 0 ? "" : "≈\(formatUsdValue(stats.balances.overallRevenue, rate: stats.usdRate))", .generic), - true - ) - - height += topLeftItemLayoutAndApply!.0.height * 3.0 + verticalSpacing * 2.0 + if let additionalStats = item.additionalStats as? StarsRevenueStats, additionalStats.balances.overallRevenue > 0 { + twoColumnLayout = true + useMinLeftColumnWidth = true + + topLeftItemLayoutAndApply = makeTopLeftItemLayout( + item.context, + params.width, + item.presentationData, + formatBalanceText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + item.presentationData.strings.Monetization_StarsProceeds_Available, + (stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))", .generic), + .ton + ) + + middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout( + item.context, + params.width, + item.presentationData, + formatBalanceText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + item.presentationData.strings.Monetization_StarsProceeds_Current, + (stats.balances.currentBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.currentBalance, rate: stats.usdRate))", .generic), + .ton + ) + + middle2LeftItemLayoutAndApply = makeMiddle2LeftItemLayout( + item.context, + params.width, + item.presentationData, + formatBalanceText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + item.presentationData.strings.Monetization_StarsProceeds_Total, + (stats.balances.overallRevenue == 0 ? "" : "≈\(formatUsdValue(stats.balances.overallRevenue, rate: stats.usdRate))", .generic), + .ton + ) + + topRightItemLayoutAndApply = makeTopRightItemLayout( + item.context, + params.width, + item.presentationData, + presentationStringsFormattedNumber(Int32(additionalStats.balances.availableBalance), item.presentationData.dateTimeFormat.groupingSeparator), + " ", + (additionalStats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(additionalStats.balances.availableBalance, divide: false, rate: additionalStats.usdRate))", .generic), + .stars + ) + + middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout( + item.context, + params.width, + item.presentationData, + presentationStringsFormattedNumber(Int32(additionalStats.balances.currentBalance), item.presentationData.dateTimeFormat.groupingSeparator), + " ", + (additionalStats.balances.currentBalance == 0 ? "" : "≈\(formatUsdValue(additionalStats.balances.currentBalance, divide: false, rate: additionalStats.usdRate))", .generic), + .stars + ) + + middle2RightItemLayoutAndApply = makeMiddle2RightItemLayout( + item.context, + params.width, + item.presentationData, + presentationStringsFormattedNumber(Int32(additionalStats.balances.overallRevenue), item.presentationData.dateTimeFormat.groupingSeparator), + " ", + (additionalStats.balances.overallRevenue == 0 ? "" : "≈\(formatUsdValue(additionalStats.balances.overallRevenue, divide: false, rate: additionalStats.usdRate))", .generic), + .stars + ) + + height += topLeftItemLayoutAndApply!.0.height * 3.0 + verticalSpacing * 2.0 + } else { + twoColumnLayout = false + + topLeftItemLayoutAndApply = makeTopLeftItemLayout( + item.context, + params.width, + item.presentationData, + formatBalanceText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + item.presentationData.strings.Monetization_Overview_Available, + (stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))", .generic), + .ton + ) + + topRightItemLayoutAndApply = makeTopRightItemLayout( + item.context, + params.width, + item.presentationData, + formatBalanceText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + item.presentationData.strings.Monetization_Overview_Current, + (stats.balances.currentBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.currentBalance, rate: stats.usdRate))", .generic), + .ton + ) + + middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout( + item.context, + params.width, + item.presentationData, + formatBalanceText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + item.presentationData.strings.Monetization_Overview_Total, + (stats.balances.overallRevenue == 0 ? "" : "≈\(formatUsdValue(stats.balances.overallRevenue, rate: stats.usdRate))", .generic), + .ton + ) + + height += topLeftItemLayoutAndApply!.0.height * 3.0 + verticalSpacing * 2.0 + } } let contentSize = CGSize(width: params.width, height: height) @@ -858,7 +962,11 @@ class StatsOverviewItemNode: ListViewItemNode { if let bottomLeftItemLayout = bottomLeftItemLayoutAndApply?.0 { maxLeftWidth = max(maxLeftWidth, bottomLeftItemLayout.width) } - secondColumnX = max(layout.size.width / 2.0, firstColumnX + maxLeftWidth + horizontalSpacing) + if useMinLeftColumnWidth { + secondColumnX = min(layout.size.width / 2.0, firstColumnX + maxLeftWidth + horizontalSpacing * 3.0) + } else { + secondColumnX = max(layout.size.width / 2.0, firstColumnX + maxLeftWidth + horizontalSpacing) + } } if let topLeftItemLayout = topLeftItemLayoutAndApply?.0 { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index c81a2d32b9c..46ce36c45dd 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -54,7 +54,6 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[571523412] = { return $0.readDouble() } dict[-1255641564] = { return parseString($0) } dict[-1194283041] = { return Api.AccountDaysTTL.parse_accountDaysTTL($0) } - dict[1008422669] = { return Api.AppWebViewResult.parse_appWebViewResultUrl($0) } dict[-653423106] = { return Api.AttachMenuBot.parse_attachMenuBot($0) } dict[-1297663893] = { return Api.AttachMenuBotIcon.parse_attachMenuBotIcon($0) } dict[1165423600] = { return Api.AttachMenuBotIconColor.parse_attachMenuBotIconColor($0) } @@ -244,7 +243,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1815593308] = { return Api.DocumentAttribute.parse_documentAttributeImageSize($0) } dict[1662637586] = { return Api.DocumentAttribute.parse_documentAttributeSticker($0) } dict[-745541182] = { return Api.DocumentAttribute.parse_documentAttributeVideo($0) } - dict[1070397423] = { return Api.DraftMessage.parse_draftMessage($0) } + dict[761606687] = { return Api.DraftMessage.parse_draftMessage($0) } dict[453805082] = { return Api.DraftMessage.parse_draftMessageEmpty($0) } dict[-1764723459] = { return Api.EmailVerification.parse_emailVerificationApple($0) } dict[-1842457175] = { return Api.EmailVerification.parse_emailVerificationCode($0) } @@ -286,9 +285,11 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-373643672] = { return Api.FolderPeer.parse_folderPeer($0) } dict[1903173033] = { return Api.ForumTopic.parse_forumTopic($0) } dict[37687451] = { return Api.ForumTopic.parse_forumTopicDeleted($0) } + dict[-394605632] = { return Api.FoundStory.parse_foundStory($0) } dict[-1107729093] = { return Api.Game.parse_game($0) } dict[-1297942941] = { return Api.GeoPoint.parse_geoPoint($0) } dict[286776671] = { return Api.GeoPoint.parse_geoPointEmpty($0) } + dict[-565420653] = { return Api.GeoPointAddress.parse_geoPointAddress($0) } dict[1934380235] = { return Api.GlobalPrivacySettings.parse_globalPrivacySettings($0) } dict[-711498484] = { return Api.GroupCall.parse_groupCall($0) } dict[2004925620] = { return Api.GroupCall.parse_groupCallDiscarded($0) } @@ -382,6 +383,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1759532989] = { return Api.InputMedia.parse_inputMediaGeoLive($0) } dict[-104578748] = { return Api.InputMedia.parse_inputMediaGeoPoint($0) } dict[1080028941] = { return Api.InputMedia.parse_inputMediaInvoice($0) } + dict[-1436147773] = { return Api.InputMedia.parse_inputMediaPaidMedia($0) } dict[-1279654347] = { return Api.InputMedia.parse_inputMediaPhoto($0) } dict[-440664550] = { return Api.InputMedia.parse_inputMediaPhotoExternal($0) } dict[261416433] = { return Api.InputMedia.parse_inputMediaPoll($0) } @@ -443,6 +445,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[859091184] = { return Api.InputSecureFile.parse_inputSecureFileUploaded($0) } dict[-618540889] = { return Api.InputSecureValue.parse_inputSecureValue($0) } dict[482797855] = { return Api.InputSingleMedia.parse_inputSingleMedia($0) } + dict[543876817] = { return Api.InputStarsTransaction.parse_inputStarsTransaction($0) } dict[42402760] = { return Api.InputStickerSet.parse_inputStickerSetAnimatedEmoji($0) } dict[215889721] = { return Api.InputStickerSet.parse_inputStickerSetAnimatedEmojiAnimations($0) } dict[-427863538] = { return Api.InputStickerSet.parse_inputStickerSetDice($0) } @@ -512,10 +515,11 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[577893055] = { return Api.MediaArea.parse_inputMediaAreaChannelPost($0) } dict[-1300094593] = { return Api.MediaArea.parse_inputMediaAreaVenue($0) } dict[1996756655] = { return Api.MediaArea.parse_mediaAreaChannelPost($0) } - dict[-544523486] = { return Api.MediaArea.parse_mediaAreaGeoPoint($0) } + dict[-891992787] = { return Api.MediaArea.parse_mediaAreaGeoPoint($0) } dict[340088945] = { return Api.MediaArea.parse_mediaAreaSuggestedReaction($0) } + dict[926421125] = { return Api.MediaArea.parse_mediaAreaUrl($0) } dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($0) } - dict[64088654] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) } + dict[-808853502] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) } dict[-1808510398] = { return Api.Message.parse_message($0) } dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) } dict[721967202] = { return Api.Message.parse_messageService($0) } @@ -596,6 +600,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-626162256] = { return Api.MessageMedia.parse_messageMediaGiveaway($0) } dict[-963047320] = { return Api.MessageMedia.parse_messageMediaGiveawayResults($0) } dict[-156940077] = { return Api.MessageMedia.parse_messageMediaInvoice($0) } + dict[-1467669359] = { return Api.MessageMedia.parse_messageMediaPaidMedia($0) } dict[1766936791] = { return Api.MessageMedia.parse_messageMediaPhoto($0) } dict[1272375192] = { return Api.MessageMedia.parse_messageMediaPoll($0) } dict[1758159491] = { return Api.MessageMedia.parse_messageMediaStory($0) } @@ -865,13 +870,14 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-378127636] = { return Api.SendMessageAction.parse_sendMessageUploadVideoAction($0) } dict[-651419003] = { return Api.SendMessageAction.parse_speakingInGroupCallAction($0) } dict[-1239335713] = { return Api.ShippingOption.parse_shippingOption($0) } - dict[-2010155333] = { return Api.SimpleWebViewResult.parse_simpleWebViewResultUrl($0) } dict[-425595208] = { return Api.SmsJob.parse_smsJob($0) } dict[-1108478618] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) } + dict[2033461574] = { return Api.StarsRevenueStatus.parse_starsRevenueStatus($0) } dict[198776256] = { return Api.StarsTopupOption.parse_starsTopupOption($0) } - dict[-865044046] = { return Api.StarsTransaction.parse_starsTransaction($0) } + dict[766853519] = { return Api.StarsTransaction.parse_starsTransaction($0) } dict[-670195363] = { return Api.StarsTransactionPeer.parse_starsTransactionPeer($0) } + dict[1617438738] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAds($0) } dict[-1269320843] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAppStore($0) } dict[-382740222] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerFragment($0) } dict[2069236235] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerPlayMarket($0) } @@ -941,6 +947,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-2095595325] = { return Api.Update.parse_updateBotWebhookJSON($0) } dict[-1684914010] = { return Api.Update.parse_updateBotWebhookJSONQuery($0) } dict[-539401739] = { return Api.Update.parse_updateBroadcastRevenueTransactions($0) } + dict[513998247] = { return Api.Update.parse_updateBusinessBotCallbackQuery($0) } dict[1666927625] = { return Api.Update.parse_updateChannel($0) } dict[-1304443240] = { return Api.Update.parse_updateChannelAvailableMessages($0) } dict[-761649164] = { return Api.Update.parse_updateChannelMessageForwards($0) } @@ -990,7 +997,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1442983757] = { return Api.Update.parse_updateLangPack($0) } dict[1180041828] = { return Api.Update.parse_updateLangPackTooLong($0) } dict[1448076945] = { return Api.Update.parse_updateLoginToken($0) } - dict[1517529484] = { return Api.Update.parse_updateMessageExtendedMedia($0) } + dict[-710666460] = { return Api.Update.parse_updateMessageExtendedMedia($0) } dict[1318109142] = { return Api.Update.parse_updateMessageID($0) } dict[-1398708869] = { return Api.Update.parse_updateMessagePoll($0) } dict[619974263] = { return Api.Update.parse_updateMessagePollVote($0) } @@ -1042,6 +1049,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-337352679] = { return Api.Update.parse_updateServiceNotification($0) } dict[-245208620] = { return Api.Update.parse_updateSmsJob($0) } dict[263737752] = { return Api.Update.parse_updateStarsBalance($0) } + dict[-1518030823] = { return Api.Update.parse_updateStarsRevenueStatus($0) } dict[834816008] = { return Api.Update.parse_updateStickerSets($0) } dict[196268545] = { return Api.Update.parse_updateStickerSetsOrder($0) } dict[738741697] = { return Api.Update.parse_updateStoriesStealthMode($0) } @@ -1096,7 +1104,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[781501415] = { return Api.WebPageAttribute.parse_webPageAttributeStory($0) } dict[1421174295] = { return Api.WebPageAttribute.parse_webPageAttributeTheme($0) } dict[211046684] = { return Api.WebViewMessageSent.parse_webViewMessageSent($0) } - dict[202659196] = { return Api.WebViewResult.parse_webViewResultUrl($0) } + dict[1294139288] = { return Api.WebViewResult.parse_webViewResultUrl($0) } dict[-1389486888] = { return Api.account.AuthorizationForm.parse_authorizationForm($0) } dict[1275039392] = { return Api.account.Authorizations.parse_authorizations($0) } dict[1674235686] = { return Api.account.AutoDownloadSettings.parse_autoDownloadSettings($0) } @@ -1146,7 +1154,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1035688326] = { return Api.auth.SentCodeType.parse_sentCodeTypeApp($0) } dict[1398007207] = { return Api.auth.SentCodeType.parse_sentCodeTypeCall($0) } dict[-196020837] = { return Api.auth.SentCodeType.parse_sentCodeTypeEmailCode($0) } - dict[331943703] = { return Api.auth.SentCodeType.parse_sentCodeTypeFirebaseSms($0) } + dict[10475318] = { return Api.auth.SentCodeType.parse_sentCodeTypeFirebaseSms($0) } dict[-1425815847] = { return Api.auth.SentCodeType.parse_sentCodeTypeFlashCall($0) } dict[-648651719] = { return Api.auth.SentCodeType.parse_sentCodeTypeFragmentSms($0) } dict[-2113903484] = { return Api.auth.SentCodeType.parse_sentCodeTypeMissedCall($0) } @@ -1306,6 +1314,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) } dict[-666824391] = { return Api.payments.PaymentResult.parse_paymentVerificationNeeded($0) } dict[-74456004] = { return Api.payments.SavedInfo.parse_savedInfo($0) } + dict[961445665] = { return Api.payments.StarsRevenueAdsAccountUrl.parse_starsRevenueAdsAccountUrl($0) } + dict[-919881925] = { return Api.payments.StarsRevenueStats.parse_starsRevenueStats($0) } + dict[497778871] = { return Api.payments.StarsRevenueWithdrawalUrl.parse_starsRevenueWithdrawalUrl($0) } dict[-1930105248] = { return Api.payments.StarsStatus.parse_starsStatus($0) } dict[-784000893] = { return Api.payments.ValidatedRequestedInfo.parse_validatedRequestedInfo($0) } dict[541839704] = { return Api.phone.ExportedGroupCallInvite.parse_exportedGroupCallInvite($0) } @@ -1344,6 +1355,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[276907596] = { return Api.storage.FileType.parse_fileWebp($0) } dict[1862033025] = { return Api.stories.AllStories.parse_allStories($0) } dict[291044926] = { return Api.stories.AllStories.parse_allStoriesNotModified($0) } + dict[-488736969] = { return Api.stories.FoundStories.parse_foundStories($0) } dict[-890861720] = { return Api.stories.PeerStories.parse_peerStories($0) } dict[1673780490] = { return Api.stories.Stories.parse_stories($0) } dict[-1436583780] = { return Api.stories.StoryReactionsList.parse_storyReactionsList($0) } @@ -1424,8 +1436,6 @@ public extension Api { switch object { case let _1 as Api.AccountDaysTTL: _1.serialize(buffer, boxed) - case let _1 as Api.AppWebViewResult: - _1.serialize(buffer, boxed) case let _1 as Api.AttachMenuBot: _1.serialize(buffer, boxed) case let _1 as Api.AttachMenuBotIcon: @@ -1618,10 +1628,14 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.ForumTopic: _1.serialize(buffer, boxed) + case let _1 as Api.FoundStory: + _1.serialize(buffer, boxed) case let _1 as Api.Game: _1.serialize(buffer, boxed) case let _1 as Api.GeoPoint: _1.serialize(buffer, boxed) + case let _1 as Api.GeoPointAddress: + _1.serialize(buffer, boxed) case let _1 as Api.GlobalPrivacySettings: _1.serialize(buffer, boxed) case let _1 as Api.GroupCall: @@ -1732,6 +1746,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.InputSingleMedia: _1.serialize(buffer, boxed) + case let _1 as Api.InputStarsTransaction: + _1.serialize(buffer, boxed) case let _1 as Api.InputStickerSet: _1.serialize(buffer, boxed) case let _1 as Api.InputStickerSetItem: @@ -1962,14 +1978,14 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.ShippingOption: _1.serialize(buffer, boxed) - case let _1 as Api.SimpleWebViewResult: - _1.serialize(buffer, boxed) case let _1 as Api.SmsJob: _1.serialize(buffer, boxed) case let _1 as Api.SponsoredMessage: _1.serialize(buffer, boxed) case let _1 as Api.SponsoredMessageReportOption: _1.serialize(buffer, boxed) + case let _1 as Api.StarsRevenueStatus: + _1.serialize(buffer, boxed) case let _1 as Api.StarsTopupOption: _1.serialize(buffer, boxed) case let _1 as Api.StarsTransaction: @@ -2326,6 +2342,12 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.payments.SavedInfo: _1.serialize(buffer, boxed) + case let _1 as Api.payments.StarsRevenueAdsAccountUrl: + _1.serialize(buffer, boxed) + case let _1 as Api.payments.StarsRevenueStats: + _1.serialize(buffer, boxed) + case let _1 as Api.payments.StarsRevenueWithdrawalUrl: + _1.serialize(buffer, boxed) case let _1 as Api.payments.StarsStatus: _1.serialize(buffer, boxed) case let _1 as Api.payments.ValidatedRequestedInfo: @@ -2380,6 +2402,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.stories.AllStories: _1.serialize(buffer, boxed) + case let _1 as Api.stories.FoundStories: + _1.serialize(buffer, boxed) case let _1 as Api.stories.PeerStories: _1.serialize(buffer, boxed) case let _1 as Api.stories.Stories: diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index 425a83a5333..2a79ef6f9e1 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -34,42 +34,6 @@ public extension Api { } } -public extension Api { - enum AppWebViewResult: TypeConstructorDescription { - case appWebViewResultUrl(url: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .appWebViewResultUrl(let url): - if boxed { - buffer.appendInt32(1008422669) - } - serializeString(url, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .appWebViewResultUrl(let url): - return ("appWebViewResultUrl", [("url", url as Any)]) - } - } - - public static func parse_appWebViewResultUrl(_ reader: BufferReader) -> AppWebViewResult? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.AppWebViewResult.appWebViewResultUrl(url: _1!) - } - else { - return nil - } - } - - } -} public extension Api { enum AttachMenuBot: TypeConstructorDescription { case attachMenuBot(flags: Int32, botId: Int64, shortName: String, peerTypes: [Api.AttachMenuPeerType]?, icons: [Api.AttachMenuBotIcon]) @@ -1196,3 +1160,43 @@ public extension Api { } } +public extension Api { + enum BotCommand: TypeConstructorDescription { + case botCommand(command: String, description: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .botCommand(let command, let description): + if boxed { + buffer.appendInt32(-1032140601) + } + serializeString(command, buffer: buffer, boxed: false) + serializeString(description, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .botCommand(let command, let description): + return ("botCommand", [("command", command as Any), ("description", description as Any)]) + } + } + + public static func parse_botCommand(_ reader: BufferReader) -> BotCommand? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.BotCommand.botCommand(command: _1!, description: _2!) + } + else { + return nil + } + } + + } +} diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index 59a0fae1234..eefc46b7430 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -9,6 +9,7 @@ public extension Api { case inputMediaGeoLive(flags: Int32, geoPoint: Api.InputGeoPoint, heading: Int32?, period: Int32?, proximityNotificationRadius: Int32?) case inputMediaGeoPoint(geoPoint: Api.InputGeoPoint) case inputMediaInvoice(flags: Int32, title: String, description: String, photo: Api.InputWebDocument?, invoice: Api.Invoice, payload: Buffer, provider: String?, providerData: Api.DataJSON, startParam: String?, extendedMedia: Api.InputMedia?) + case inputMediaPaidMedia(starsAmount: Int64, extendedMedia: [Api.InputMedia]) case inputMediaPhoto(flags: Int32, id: Api.InputPhoto, ttlSeconds: Int32?) case inputMediaPhotoExternal(flags: Int32, url: String, ttlSeconds: Int32?) case inputMediaPoll(flags: Int32, poll: Api.Poll, correctAnswers: [Buffer]?, solution: String?, solutionEntities: [Api.MessageEntity]?) @@ -95,6 +96,17 @@ public extension Api { if Int(flags) & Int(1 << 1) != 0 {serializeString(startParam!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 2) != 0 {extendedMedia!.serialize(buffer, true)} break + case .inputMediaPaidMedia(let starsAmount, let extendedMedia): + if boxed { + buffer.appendInt32(-1436147773) + } + serializeInt64(starsAmount, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(extendedMedia.count)) + for item in extendedMedia { + item.serialize(buffer, true) + } + break case .inputMediaPhoto(let flags, let id, let ttlSeconds): if boxed { buffer.appendInt32(-1279654347) @@ -210,6 +222,8 @@ public extension Api { return ("inputMediaGeoPoint", [("geoPoint", geoPoint as Any)]) case .inputMediaInvoice(let flags, let title, let description, let photo, let invoice, let payload, let provider, let providerData, let startParam, let extendedMedia): return ("inputMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("payload", payload as Any), ("provider", provider as Any), ("providerData", providerData as Any), ("startParam", startParam as Any), ("extendedMedia", extendedMedia as Any)]) + case .inputMediaPaidMedia(let starsAmount, let extendedMedia): + return ("inputMediaPaidMedia", [("starsAmount", starsAmount as Any), ("extendedMedia", extendedMedia as Any)]) case .inputMediaPhoto(let flags, let id, let ttlSeconds): return ("inputMediaPhoto", [("flags", flags as Any), ("id", id as Any), ("ttlSeconds", ttlSeconds as Any)]) case .inputMediaPhotoExternal(let flags, let url, let ttlSeconds): @@ -399,6 +413,22 @@ public extension Api { return nil } } + public static func parse_inputMediaPaidMedia(_ reader: BufferReader) -> InputMedia? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.InputMedia]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputMedia.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputMedia.inputMediaPaidMedia(starsAmount: _1!, extendedMedia: _2!) + } + else { + return nil + } + } public static func parse_inputMediaPhoto(_ reader: BufferReader) -> InputMedia? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index b318ced6ef0..66a1ef38225 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -58,6 +58,46 @@ public extension Api { } } +public extension Api { + enum InputStarsTransaction: TypeConstructorDescription { + case inputStarsTransaction(flags: Int32, id: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputStarsTransaction(let flags, let id): + if boxed { + buffer.appendInt32(543876817) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(id, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputStarsTransaction(let flags, let id): + return ("inputStarsTransaction", [("flags", flags as Any), ("id", id as Any)]) + } + } + + public static func parse_inputStarsTransaction(_ reader: BufferReader) -> InputStarsTransaction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputStarsTransaction.inputStarsTransaction(flags: _1!, id: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum InputStickerSet: TypeConstructorDescription { case inputStickerSetAnimatedEmoji @@ -918,119 +958,3 @@ public extension Api { } } -public extension Api { - enum InputWebFileLocation: TypeConstructorDescription { - case inputWebFileAudioAlbumThumbLocation(flags: Int32, document: Api.InputDocument?, title: String?, performer: String?) - case inputWebFileGeoPointLocation(geoPoint: Api.InputGeoPoint, accessHash: Int64, w: Int32, h: Int32, zoom: Int32, scale: Int32) - case inputWebFileLocation(url: String, accessHash: Int64) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputWebFileAudioAlbumThumbLocation(let flags, let document, let title, let performer): - if boxed { - buffer.appendInt32(-193992412) - } - serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {document!.serialize(buffer, true)} - if Int(flags) & Int(1 << 1) != 0 {serializeString(title!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {serializeString(performer!, buffer: buffer, boxed: false)} - break - case .inputWebFileGeoPointLocation(let geoPoint, let accessHash, let w, let h, let zoom, let scale): - if boxed { - buffer.appendInt32(-1625153079) - } - geoPoint.serialize(buffer, true) - serializeInt64(accessHash, buffer: buffer, boxed: false) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeInt32(zoom, buffer: buffer, boxed: false) - serializeInt32(scale, buffer: buffer, boxed: false) - break - case .inputWebFileLocation(let url, let accessHash): - if boxed { - buffer.appendInt32(-1036396922) - } - serializeString(url, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputWebFileAudioAlbumThumbLocation(let flags, let document, let title, let performer): - return ("inputWebFileAudioAlbumThumbLocation", [("flags", flags as Any), ("document", document as Any), ("title", title as Any), ("performer", performer as Any)]) - case .inputWebFileGeoPointLocation(let geoPoint, let accessHash, let w, let h, let zoom, let scale): - return ("inputWebFileGeoPointLocation", [("geoPoint", geoPoint as Any), ("accessHash", accessHash as Any), ("w", w as Any), ("h", h as Any), ("zoom", zoom as Any), ("scale", scale as Any)]) - case .inputWebFileLocation(let url, let accessHash): - return ("inputWebFileLocation", [("url", url as Any), ("accessHash", accessHash as Any)]) - } - } - - public static func parse_inputWebFileAudioAlbumThumbLocation(_ reader: BufferReader) -> InputWebFileLocation? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.InputDocument? - if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.InputDocument - } } - var _3: String? - if Int(_1!) & Int(1 << 1) != 0 {_3 = parseString(reader) } - var _4: String? - if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.InputWebFileLocation.inputWebFileAudioAlbumThumbLocation(flags: _1!, document: _2, title: _3, performer: _4) - } - else { - return nil - } - } - public static func parse_inputWebFileGeoPointLocation(_ reader: BufferReader) -> InputWebFileLocation? { - var _1: Api.InputGeoPoint? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputGeoPoint - } - var _2: Int64? - _2 = reader.readInt64() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int32? - _5 = reader.readInt32() - var _6: Int32? - _6 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.InputWebFileLocation.inputWebFileGeoPointLocation(geoPoint: _1!, accessHash: _2!, w: _3!, h: _4!, zoom: _5!, scale: _6!) - } - else { - return nil - } - } - public static func parse_inputWebFileLocation(_ reader: BufferReader) -> InputWebFileLocation? { - var _1: String? - _1 = parseString(reader) - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputWebFileLocation.inputWebFileLocation(url: _1!, accessHash: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api13.swift b/submodules/TelegramApi/Sources/Api13.swift index 39e9b04e848..ecddafbffd8 100644 --- a/submodules/TelegramApi/Sources/Api13.swift +++ b/submodules/TelegramApi/Sources/Api13.swift @@ -1,3 +1,119 @@ +public extension Api { + enum InputWebFileLocation: TypeConstructorDescription { + case inputWebFileAudioAlbumThumbLocation(flags: Int32, document: Api.InputDocument?, title: String?, performer: String?) + case inputWebFileGeoPointLocation(geoPoint: Api.InputGeoPoint, accessHash: Int64, w: Int32, h: Int32, zoom: Int32, scale: Int32) + case inputWebFileLocation(url: String, accessHash: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputWebFileAudioAlbumThumbLocation(let flags, let document, let title, let performer): + if boxed { + buffer.appendInt32(-193992412) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {document!.serialize(buffer, true)} + if Int(flags) & Int(1 << 1) != 0 {serializeString(title!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeString(performer!, buffer: buffer, boxed: false)} + break + case .inputWebFileGeoPointLocation(let geoPoint, let accessHash, let w, let h, let zoom, let scale): + if boxed { + buffer.appendInt32(-1625153079) + } + geoPoint.serialize(buffer, true) + serializeInt64(accessHash, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(zoom, buffer: buffer, boxed: false) + serializeInt32(scale, buffer: buffer, boxed: false) + break + case .inputWebFileLocation(let url, let accessHash): + if boxed { + buffer.appendInt32(-1036396922) + } + serializeString(url, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputWebFileAudioAlbumThumbLocation(let flags, let document, let title, let performer): + return ("inputWebFileAudioAlbumThumbLocation", [("flags", flags as Any), ("document", document as Any), ("title", title as Any), ("performer", performer as Any)]) + case .inputWebFileGeoPointLocation(let geoPoint, let accessHash, let w, let h, let zoom, let scale): + return ("inputWebFileGeoPointLocation", [("geoPoint", geoPoint as Any), ("accessHash", accessHash as Any), ("w", w as Any), ("h", h as Any), ("zoom", zoom as Any), ("scale", scale as Any)]) + case .inputWebFileLocation(let url, let accessHash): + return ("inputWebFileLocation", [("url", url as Any), ("accessHash", accessHash as Any)]) + } + } + + public static func parse_inputWebFileAudioAlbumThumbLocation(_ reader: BufferReader) -> InputWebFileLocation? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.InputDocument? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.InputDocument + } } + var _3: String? + if Int(_1!) & Int(1 << 1) != 0 {_3 = parseString(reader) } + var _4: String? + if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputWebFileLocation.inputWebFileAudioAlbumThumbLocation(flags: _1!, document: _2, title: _3, performer: _4) + } + else { + return nil + } + } + public static func parse_inputWebFileGeoPointLocation(_ reader: BufferReader) -> InputWebFileLocation? { + var _1: Api.InputGeoPoint? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputGeoPoint + } + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.InputWebFileLocation.inputWebFileGeoPointLocation(geoPoint: _1!, accessHash: _2!, w: _3!, h: _4!, zoom: _5!, scale: _6!) + } + else { + return nil + } + } + public static func parse_inputWebFileLocation(_ reader: BufferReader) -> InputWebFileLocation? { + var _1: String? + _1 = parseString(reader) + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputWebFileLocation.inputWebFileLocation(url: _1!, accessHash: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum Invoice: TypeConstructorDescription { case invoice(flags: Int32, currency: String, prices: [Api.LabeledPrice], maxTipAmount: Int64?, suggestedTipAmounts: [Int64]?, termsUrl: String?) @@ -934,111 +1050,3 @@ public extension Api { } } -public extension Api { - enum LangPackString: TypeConstructorDescription { - case langPackString(key: String, value: String) - case langPackStringDeleted(key: String) - case langPackStringPluralized(flags: Int32, key: String, zeroValue: String?, oneValue: String?, twoValue: String?, fewValue: String?, manyValue: String?, otherValue: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .langPackString(let key, let value): - if boxed { - buffer.appendInt32(-892239370) - } - serializeString(key, buffer: buffer, boxed: false) - serializeString(value, buffer: buffer, boxed: false) - break - case .langPackStringDeleted(let key): - if boxed { - buffer.appendInt32(695856818) - } - serializeString(key, buffer: buffer, boxed: false) - break - case .langPackStringPluralized(let flags, let key, let zeroValue, let oneValue, let twoValue, let fewValue, let manyValue, let otherValue): - if boxed { - buffer.appendInt32(1816636575) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(key, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeString(zeroValue!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {serializeString(oneValue!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 2) != 0 {serializeString(twoValue!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 3) != 0 {serializeString(fewValue!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 4) != 0 {serializeString(manyValue!, buffer: buffer, boxed: false)} - serializeString(otherValue, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .langPackString(let key, let value): - return ("langPackString", [("key", key as Any), ("value", value as Any)]) - case .langPackStringDeleted(let key): - return ("langPackStringDeleted", [("key", key as Any)]) - case .langPackStringPluralized(let flags, let key, let zeroValue, let oneValue, let twoValue, let fewValue, let manyValue, let otherValue): - return ("langPackStringPluralized", [("flags", flags as Any), ("key", key as Any), ("zeroValue", zeroValue as Any), ("oneValue", oneValue as Any), ("twoValue", twoValue as Any), ("fewValue", fewValue as Any), ("manyValue", manyValue as Any), ("otherValue", otherValue as Any)]) - } - } - - public static func parse_langPackString(_ reader: BufferReader) -> LangPackString? { - var _1: String? - _1 = parseString(reader) - var _2: String? - _2 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.LangPackString.langPackString(key: _1!, value: _2!) - } - else { - return nil - } - } - public static func parse_langPackStringDeleted(_ reader: BufferReader) -> LangPackString? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.LangPackString.langPackStringDeleted(key: _1!) - } - else { - return nil - } - } - public static func parse_langPackStringPluralized(_ reader: BufferReader) -> LangPackString? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: String? - if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } - var _4: String? - if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } - var _5: String? - if Int(_1!) & Int(1 << 2) != 0 {_5 = parseString(reader) } - var _6: String? - if Int(_1!) & Int(1 << 3) != 0 {_6 = parseString(reader) } - var _7: String? - if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) } - var _8: String? - _8 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil - let _c8 = _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.LangPackString.langPackStringPluralized(flags: _1!, key: _2!, zeroValue: _3, oneValue: _4, twoValue: _5, fewValue: _6, manyValue: _7, otherValue: _8!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api14.swift b/submodules/TelegramApi/Sources/Api14.swift index f9b2095a7c6..737b1ea9331 100644 --- a/submodules/TelegramApi/Sources/Api14.swift +++ b/submodules/TelegramApi/Sources/Api14.swift @@ -1,3 +1,111 @@ +public extension Api { + enum LangPackString: TypeConstructorDescription { + case langPackString(key: String, value: String) + case langPackStringDeleted(key: String) + case langPackStringPluralized(flags: Int32, key: String, zeroValue: String?, oneValue: String?, twoValue: String?, fewValue: String?, manyValue: String?, otherValue: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .langPackString(let key, let value): + if boxed { + buffer.appendInt32(-892239370) + } + serializeString(key, buffer: buffer, boxed: false) + serializeString(value, buffer: buffer, boxed: false) + break + case .langPackStringDeleted(let key): + if boxed { + buffer.appendInt32(695856818) + } + serializeString(key, buffer: buffer, boxed: false) + break + case .langPackStringPluralized(let flags, let key, let zeroValue, let oneValue, let twoValue, let fewValue, let manyValue, let otherValue): + if boxed { + buffer.appendInt32(1816636575) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(key, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(zeroValue!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeString(oneValue!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeString(twoValue!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {serializeString(fewValue!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {serializeString(manyValue!, buffer: buffer, boxed: false)} + serializeString(otherValue, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .langPackString(let key, let value): + return ("langPackString", [("key", key as Any), ("value", value as Any)]) + case .langPackStringDeleted(let key): + return ("langPackStringDeleted", [("key", key as Any)]) + case .langPackStringPluralized(let flags, let key, let zeroValue, let oneValue, let twoValue, let fewValue, let manyValue, let otherValue): + return ("langPackStringPluralized", [("flags", flags as Any), ("key", key as Any), ("zeroValue", zeroValue as Any), ("oneValue", oneValue as Any), ("twoValue", twoValue as Any), ("fewValue", fewValue as Any), ("manyValue", manyValue as Any), ("otherValue", otherValue as Any)]) + } + } + + public static func parse_langPackString(_ reader: BufferReader) -> LangPackString? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.LangPackString.langPackString(key: _1!, value: _2!) + } + else { + return nil + } + } + public static func parse_langPackStringDeleted(_ reader: BufferReader) -> LangPackString? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.LangPackString.langPackStringDeleted(key: _1!) + } + else { + return nil + } + } + public static func parse_langPackStringPluralized(_ reader: BufferReader) -> LangPackString? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } + var _4: String? + if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } + var _5: String? + if Int(_1!) & Int(1 << 2) != 0 {_5 = parseString(reader) } + var _6: String? + if Int(_1!) & Int(1 << 3) != 0 {_6 = parseString(reader) } + var _7: String? + if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) } + var _8: String? + _8 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.LangPackString.langPackStringPluralized(flags: _1!, key: _2!, zeroValue: _3, oneValue: _4, twoValue: _5, fewValue: _6, manyValue: _7, otherValue: _8!) + } + else { + return nil + } + } + + } +} public extension Api { enum MaskCoords: TypeConstructorDescription { case maskCoords(n: Int32, x: Double, y: Double, zoom: Double) @@ -51,8 +159,9 @@ public extension Api { case inputMediaAreaChannelPost(coordinates: Api.MediaAreaCoordinates, channel: Api.InputChannel, msgId: Int32) case inputMediaAreaVenue(coordinates: Api.MediaAreaCoordinates, queryId: Int64, resultId: String) case mediaAreaChannelPost(coordinates: Api.MediaAreaCoordinates, channelId: Int64, msgId: Int32) - case mediaAreaGeoPoint(coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint) + case mediaAreaGeoPoint(flags: Int32, coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint, address: Api.GeoPointAddress?) case mediaAreaSuggestedReaction(flags: Int32, coordinates: Api.MediaAreaCoordinates, reaction: Api.Reaction) + case mediaAreaUrl(coordinates: Api.MediaAreaCoordinates, url: String) case mediaAreaVenue(coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -81,12 +190,14 @@ public extension Api { serializeInt64(channelId, buffer: buffer, boxed: false) serializeInt32(msgId, buffer: buffer, boxed: false) break - case .mediaAreaGeoPoint(let coordinates, let geo): + case .mediaAreaGeoPoint(let flags, let coordinates, let geo, let address): if boxed { - buffer.appendInt32(-544523486) + buffer.appendInt32(-891992787) } + serializeInt32(flags, buffer: buffer, boxed: false) coordinates.serialize(buffer, true) geo.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 {address!.serialize(buffer, true)} break case .mediaAreaSuggestedReaction(let flags, let coordinates, let reaction): if boxed { @@ -96,6 +207,13 @@ public extension Api { coordinates.serialize(buffer, true) reaction.serialize(buffer, true) break + case .mediaAreaUrl(let coordinates, let url): + if boxed { + buffer.appendInt32(926421125) + } + coordinates.serialize(buffer, true) + serializeString(url, buffer: buffer, boxed: false) + break case .mediaAreaVenue(let coordinates, let geo, let title, let address, let provider, let venueId, let venueType): if boxed { buffer.appendInt32(-1098720356) @@ -119,10 +237,12 @@ public extension Api { return ("inputMediaAreaVenue", [("coordinates", coordinates as Any), ("queryId", queryId as Any), ("resultId", resultId as Any)]) case .mediaAreaChannelPost(let coordinates, let channelId, let msgId): return ("mediaAreaChannelPost", [("coordinates", coordinates as Any), ("channelId", channelId as Any), ("msgId", msgId as Any)]) - case .mediaAreaGeoPoint(let coordinates, let geo): - return ("mediaAreaGeoPoint", [("coordinates", coordinates as Any), ("geo", geo as Any)]) + case .mediaAreaGeoPoint(let flags, let coordinates, let geo, let address): + return ("mediaAreaGeoPoint", [("flags", flags as Any), ("coordinates", coordinates as Any), ("geo", geo as Any), ("address", address as Any)]) case .mediaAreaSuggestedReaction(let flags, let coordinates, let reaction): return ("mediaAreaSuggestedReaction", [("flags", flags as Any), ("coordinates", coordinates as Any), ("reaction", reaction as Any)]) + case .mediaAreaUrl(let coordinates, let url): + return ("mediaAreaUrl", [("coordinates", coordinates as Any), ("url", url as Any)]) case .mediaAreaVenue(let coordinates, let geo, let title, let address, let provider, let venueId, let venueType): return ("mediaAreaVenue", [("coordinates", coordinates as Any), ("geo", geo as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any)]) } @@ -188,18 +308,26 @@ public extension Api { } } public static func parse_mediaAreaGeoPoint(_ reader: BufferReader) -> MediaArea? { - var _1: Api.MediaAreaCoordinates? + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.MediaAreaCoordinates? if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates + _2 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates } - var _2: Api.GeoPoint? + var _3: Api.GeoPoint? if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.GeoPoint + _3 = Api.parse(reader, signature: signature) as? Api.GeoPoint } + var _4: Api.GeoPointAddress? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.GeoPointAddress + } } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.MediaArea.mediaAreaGeoPoint(coordinates: _1!, geo: _2!) + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.MediaArea.mediaAreaGeoPoint(flags: _1!, coordinates: _2!, geo: _3!, address: _4) } else { return nil @@ -226,6 +354,22 @@ public extension Api { return nil } } + public static func parse_mediaAreaUrl(_ reader: BufferReader) -> MediaArea? { + var _1: Api.MediaAreaCoordinates? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates + } + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.MediaArea.mediaAreaUrl(coordinates: _1!, url: _2!) + } + else { + return nil + } + } public static func parse_mediaAreaVenue(_ reader: BufferReader) -> MediaArea? { var _1: Api.MediaAreaCoordinates? if let signature = reader.readInt32() { @@ -264,33 +408,35 @@ public extension Api { } public extension Api { enum MediaAreaCoordinates: TypeConstructorDescription { - case mediaAreaCoordinates(x: Double, y: Double, w: Double, h: Double, rotation: Double) + case mediaAreaCoordinates(flags: Int32, x: Double, y: Double, w: Double, h: Double, rotation: Double, radius: Double?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .mediaAreaCoordinates(let x, let y, let w, let h, let rotation): + case .mediaAreaCoordinates(let flags, let x, let y, let w, let h, let rotation, let radius): if boxed { - buffer.appendInt32(64088654) + buffer.appendInt32(-808853502) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeDouble(x, buffer: buffer, boxed: false) serializeDouble(y, buffer: buffer, boxed: false) serializeDouble(w, buffer: buffer, boxed: false) serializeDouble(h, buffer: buffer, boxed: false) serializeDouble(rotation, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeDouble(radius!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .mediaAreaCoordinates(let x, let y, let w, let h, let rotation): - return ("mediaAreaCoordinates", [("x", x as Any), ("y", y as Any), ("w", w as Any), ("h", h as Any), ("rotation", rotation as Any)]) + case .mediaAreaCoordinates(let flags, let x, let y, let w, let h, let rotation, let radius): + return ("mediaAreaCoordinates", [("flags", flags as Any), ("x", x as Any), ("y", y as Any), ("w", w as Any), ("h", h as Any), ("rotation", rotation as Any), ("radius", radius as Any)]) } } public static func parse_mediaAreaCoordinates(_ reader: BufferReader) -> MediaAreaCoordinates? { - var _1: Double? - _1 = reader.readDouble() + var _1: Int32? + _1 = reader.readInt32() var _2: Double? _2 = reader.readDouble() var _3: Double? @@ -299,13 +445,19 @@ public extension Api { _4 = reader.readDouble() var _5: Double? _5 = reader.readDouble() + var _6: Double? + _6 = reader.readDouble() + var _7: Double? + if Int(_1!) & Int(1 << 0) != 0 {_7 = reader.readDouble() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.MediaAreaCoordinates.mediaAreaCoordinates(x: _1!, y: _2!, w: _3!, h: _4!, rotation: _5!) + let _c6 = _6 != nil + let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.MediaAreaCoordinates.mediaAreaCoordinates(flags: _1!, x: _2!, y: _3!, w: _4!, h: _5!, rotation: _6!, radius: _7) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index 3924397ce3a..334a7490250 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -718,6 +718,7 @@ public extension Api { case messageMediaGiveaway(flags: Int32, channels: [Int64], countriesIso2: [String]?, prizeDescription: String?, quantity: Int32, months: Int32, untilDate: Int32) case messageMediaGiveawayResults(flags: Int32, channelId: Int64, additionalPeersCount: Int32?, launchMsgId: Int32, winnersCount: Int32, unclaimedCount: Int32, winners: [Int64], months: Int32, prizeDescription: String?, untilDate: Int32) case messageMediaInvoice(flags: Int32, title: String, description: String, photo: Api.WebDocument?, receiptMsgId: Int32?, currency: String, totalAmount: Int64, startParam: String, extendedMedia: Api.MessageExtendedMedia?) + case messageMediaPaidMedia(starsAmount: Int64, extendedMedia: [Api.MessageExtendedMedia]) case messageMediaPhoto(flags: Int32, photo: Api.Photo?, ttlSeconds: Int32?) case messageMediaPoll(poll: Api.Poll, results: Api.PollResults) case messageMediaStory(flags: Int32, peer: Api.Peer, id: Int32, story: Api.StoryItem?) @@ -834,6 +835,17 @@ public extension Api { serializeString(startParam, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 4) != 0 {extendedMedia!.serialize(buffer, true)} break + case .messageMediaPaidMedia(let starsAmount, let extendedMedia): + if boxed { + buffer.appendInt32(-1467669359) + } + serializeInt64(starsAmount, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(extendedMedia.count)) + for item in extendedMedia { + item.serialize(buffer, true) + } + break case .messageMediaPhoto(let flags, let photo, let ttlSeconds): if boxed { buffer.appendInt32(1766936791) @@ -907,6 +919,8 @@ public extension Api { return ("messageMediaGiveawayResults", [("flags", flags as Any), ("channelId", channelId as Any), ("additionalPeersCount", additionalPeersCount as Any), ("launchMsgId", launchMsgId as Any), ("winnersCount", winnersCount as Any), ("unclaimedCount", unclaimedCount as Any), ("winners", winners as Any), ("months", months as Any), ("prizeDescription", prizeDescription as Any), ("untilDate", untilDate as Any)]) case .messageMediaInvoice(let flags, let title, let description, let photo, let receiptMsgId, let currency, let totalAmount, let startParam, let extendedMedia): return ("messageMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("receiptMsgId", receiptMsgId as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("startParam", startParam as Any), ("extendedMedia", extendedMedia as Any)]) + case .messageMediaPaidMedia(let starsAmount, let extendedMedia): + return ("messageMediaPaidMedia", [("starsAmount", starsAmount as Any), ("extendedMedia", extendedMedia as Any)]) case .messageMediaPhoto(let flags, let photo, let ttlSeconds): return ("messageMediaPhoto", [("flags", flags as Any), ("photo", photo as Any), ("ttlSeconds", ttlSeconds as Any)]) case .messageMediaPoll(let poll, let results): @@ -1149,6 +1163,22 @@ public extension Api { return nil } } + public static func parse_messageMediaPaidMedia(_ reader: BufferReader) -> MessageMedia? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.MessageExtendedMedia]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageExtendedMedia.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.MessageMedia.messageMediaPaidMedia(starsAmount: _1!, extendedMedia: _2!) + } + else { + return nil + } + } public static func parse_messageMediaPhoto(_ reader: BufferReader) -> MessageMedia? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 0880c7cd5a9..51d1fb1d7fe 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -1,43 +1,3 @@ -public extension Api { - enum BotCommand: TypeConstructorDescription { - case botCommand(command: String, description: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .botCommand(let command, let description): - if boxed { - buffer.appendInt32(-1032140601) - } - serializeString(command, buffer: buffer, boxed: false) - serializeString(description, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .botCommand(let command, let description): - return ("botCommand", [("command", command as Any), ("description", description as Any)]) - } - } - - public static func parse_botCommand(_ reader: BufferReader) -> BotCommand? { - var _1: String? - _1 = parseString(reader) - var _2: String? - _2 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.BotCommand.botCommand(command: _1!, description: _2!) - } - else { - return nil - } - } - - } -} public extension Api { indirect enum BotCommandScope: TypeConstructorDescription { case botCommandScopeChatAdmins @@ -1200,3 +1160,53 @@ public extension Api { } } +public extension Api { + enum BusinessIntro: TypeConstructorDescription { + case businessIntro(flags: Int32, title: String, description: String, sticker: Api.Document?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessIntro(let flags, let title, let description, let sticker): + if boxed { + buffer.appendInt32(1510606445) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(description, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {sticker!.serialize(buffer, true)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .businessIntro(let flags, let title, let description, let sticker): + return ("businessIntro", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("sticker", sticker as Any)]) + } + } + + public static func parse_businessIntro(_ reader: BufferReader) -> BusinessIntro? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: Api.Document? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.Document + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.BusinessIntro.businessIntro(flags: _1!, title: _2!, description: _3!, sticker: _4) + } + else { + return nil + } + } + + } +} diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index 733ac161c8c..3a9094cca90 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -396,42 +396,6 @@ public extension Api { } } -public extension Api { - enum SimpleWebViewResult: TypeConstructorDescription { - case simpleWebViewResultUrl(url: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .simpleWebViewResultUrl(let url): - if boxed { - buffer.appendInt32(-2010155333) - } - serializeString(url, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .simpleWebViewResultUrl(let url): - return ("simpleWebViewResultUrl", [("url", url as Any)]) - } - } - - public static func parse_simpleWebViewResultUrl(_ reader: BufferReader) -> SimpleWebViewResult? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.SimpleWebViewResult.simpleWebViewResultUrl(url: _1!) - } - else { - return nil - } - } - - } -} public extension Api { enum SmsJob: TypeConstructorDescription { case smsJob(jobId: String, phoneNumber: String, text: String) @@ -602,6 +566,58 @@ public extension Api { } } +public extension Api { + enum StarsRevenueStatus: TypeConstructorDescription { + case starsRevenueStatus(flags: Int32, currentBalance: Int64, availableBalance: Int64, overallRevenue: Int64, nextWithdrawalAt: Int32?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsRevenueStatus(let flags, let currentBalance, let availableBalance, let overallRevenue, let nextWithdrawalAt): + if boxed { + buffer.appendInt32(2033461574) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(currentBalance, buffer: buffer, boxed: false) + serializeInt64(availableBalance, buffer: buffer, boxed: false) + serializeInt64(overallRevenue, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(nextWithdrawalAt!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starsRevenueStatus(let flags, let currentBalance, let availableBalance, let overallRevenue, let nextWithdrawalAt): + return ("starsRevenueStatus", [("flags", flags as Any), ("currentBalance", currentBalance as Any), ("availableBalance", availableBalance as Any), ("overallRevenue", overallRevenue as Any), ("nextWithdrawalAt", nextWithdrawalAt as Any)]) + } + } + + public static func parse_starsRevenueStatus(_ reader: BufferReader) -> StarsRevenueStatus? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int64? + _3 = reader.readInt64() + var _4: Int64? + _4 = reader.readInt64() + var _5: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.StarsRevenueStatus.starsRevenueStatus(flags: _1!, currentBalance: _2!, availableBalance: _3!, overallRevenue: _4!, nextWithdrawalAt: _5) + } + else { + return nil + } + } + + } +} public extension Api { enum StarsTopupOption: TypeConstructorDescription { case starsTopupOption(flags: Int32, stars: Int64, storeProduct: String?, currency: String, amount: Int64) @@ -656,13 +672,13 @@ public extension Api { } public extension Api { enum StarsTransaction: TypeConstructorDescription { - case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?) + case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo): + case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia): if boxed { - buffer.appendInt32(-865044046) + buffer.appendInt32(766853519) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(id, buffer: buffer, boxed: false) @@ -672,14 +688,23 @@ public extension Api { if Int(flags) & Int(1 << 0) != 0 {serializeString(title!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 1) != 0 {serializeString(description!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 2) != 0 {photo!.serialize(buffer, true)} + if Int(flags) & Int(1 << 5) != 0 {serializeInt32(transactionDate!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 5) != 0 {serializeString(transactionUrl!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 7) != 0 {serializeBytes(botPayload!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 8) != 0 {serializeInt32(msgId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 9) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(extendedMedia!.count)) + for item in extendedMedia! { + item.serialize(buffer, true) + }} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo): - return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any)]) + case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia): + return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any)]) } } @@ -704,6 +729,18 @@ public extension Api { if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.WebDocument } } + var _9: Int32? + if Int(_1!) & Int(1 << 5) != 0 {_9 = reader.readInt32() } + var _10: String? + if Int(_1!) & Int(1 << 5) != 0 {_10 = parseString(reader) } + var _11: Buffer? + if Int(_1!) & Int(1 << 7) != 0 {_11 = parseBytes(reader) } + var _12: Int32? + if Int(_1!) & Int(1 << 8) != 0 {_12 = reader.readInt32() } + var _13: [Api.MessageMedia]? + if Int(_1!) & Int(1 << 9) != 0 {if let _ = reader.readInt32() { + _13 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageMedia.self) + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -712,8 +749,13 @@ public extension Api { let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8) + let _c9 = (Int(_1!) & Int(1 << 5) == 0) || _9 != nil + let _c10 = (Int(_1!) & Int(1 << 5) == 0) || _10 != nil + let _c11 = (Int(_1!) & Int(1 << 7) == 0) || _11 != nil + let _c12 = (Int(_1!) & Int(1 << 8) == 0) || _12 != nil + let _c13 = (Int(_1!) & Int(1 << 9) == 0) || _13 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 { + return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13) } else { return nil @@ -725,6 +767,7 @@ public extension Api { public extension Api { enum StarsTransactionPeer: TypeConstructorDescription { case starsTransactionPeer(peer: Api.Peer) + case starsTransactionPeerAds case starsTransactionPeerAppStore case starsTransactionPeerFragment case starsTransactionPeerPlayMarket @@ -738,6 +781,12 @@ public extension Api { buffer.appendInt32(-670195363) } peer.serialize(buffer, true) + break + case .starsTransactionPeerAds: + if boxed { + buffer.appendInt32(1617438738) + } + break case .starsTransactionPeerAppStore: if boxed { @@ -776,6 +825,8 @@ public extension Api { switch self { case .starsTransactionPeer(let peer): return ("starsTransactionPeer", [("peer", peer as Any)]) + case .starsTransactionPeerAds: + return ("starsTransactionPeerAds", []) case .starsTransactionPeerAppStore: return ("starsTransactionPeerAppStore", []) case .starsTransactionPeerFragment: @@ -802,6 +853,9 @@ public extension Api { return nil } } + public static func parse_starsTransactionPeerAds(_ reader: BufferReader) -> StarsTransactionPeer? { + return Api.StarsTransactionPeer.starsTransactionPeerAds + } public static func parse_starsTransactionPeerAppStore(_ reader: BufferReader) -> StarsTransactionPeer? { return Api.StarsTransactionPeer.starsTransactionPeerAppStore } diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index ffad9c1ca72..7d031ef3001 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -271,6 +271,7 @@ public extension Api { case updateBotWebhookJSON(data: Api.DataJSON) case updateBotWebhookJSONQuery(queryId: Int64, data: Api.DataJSON, timeout: Int32) case updateBroadcastRevenueTransactions(peer: Api.Peer, balances: Api.BroadcastRevenueBalances) + case updateBusinessBotCallbackQuery(flags: Int32, queryId: Int64, userId: Int64, connectionId: String, message: Api.Message, replyToMessage: Api.Message?, chatInstance: Int64, data: Buffer?) case updateChannel(channelId: Int64) case updateChannelAvailableMessages(channelId: Int64, availableMinId: Int32) case updateChannelMessageForwards(channelId: Int64, id: Int32, forwards: Int32) @@ -320,7 +321,7 @@ public extension Api { case updateLangPack(difference: Api.LangPackDifference) case updateLangPackTooLong(langCode: String) case updateLoginToken - case updateMessageExtendedMedia(peer: Api.Peer, msgId: Int32, extendedMedia: Api.MessageExtendedMedia) + case updateMessageExtendedMedia(peer: Api.Peer, msgId: Int32, extendedMedia: [Api.MessageExtendedMedia]) case updateMessageID(id: Int32, randomId: Int64) case updateMessagePoll(flags: Int32, pollId: Int64, poll: Api.Poll?, results: Api.PollResults) case updateMessagePollVote(pollId: Int64, peer: Api.Peer, options: [Buffer], qts: Int32) @@ -372,6 +373,7 @@ public extension Api { case updateServiceNotification(flags: Int32, inboxDate: Int32?, type: String, message: String, media: Api.MessageMedia, entities: [Api.MessageEntity]) case updateSmsJob(jobId: String) case updateStarsBalance(balance: Int64) + case updateStarsRevenueStatus(peer: Api.Peer, status: Api.StarsRevenueStatus) case updateStickerSets(flags: Int32) case updateStickerSetsOrder(flags: Int32, order: [Int64]) case updateStoriesStealthMode(stealthMode: Api.StoriesStealthMode) @@ -602,6 +604,19 @@ public extension Api { peer.serialize(buffer, true) balances.serialize(buffer, true) break + case .updateBusinessBotCallbackQuery(let flags, let queryId, let userId, let connectionId, let message, let replyToMessage, let chatInstance, let data): + if boxed { + buffer.appendInt32(513998247) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(queryId, buffer: buffer, boxed: false) + serializeInt64(userId, buffer: buffer, boxed: false) + serializeString(connectionId, buffer: buffer, boxed: false) + message.serialize(buffer, true) + if Int(flags) & Int(1 << 2) != 0 {replyToMessage!.serialize(buffer, true)} + serializeInt64(chatInstance, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeBytes(data!, buffer: buffer, boxed: false)} + break case .updateChannel(let channelId): if boxed { buffer.appendInt32(1666927625) @@ -1024,11 +1039,15 @@ public extension Api { break case .updateMessageExtendedMedia(let peer, let msgId, let extendedMedia): if boxed { - buffer.appendInt32(1517529484) + buffer.appendInt32(-710666460) } peer.serialize(buffer, true) serializeInt32(msgId, buffer: buffer, boxed: false) - extendedMedia.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(extendedMedia.count)) + for item in extendedMedia { + item.serialize(buffer, true) + } break case .updateMessageID(let id, let randomId): if boxed { @@ -1460,6 +1479,13 @@ public extension Api { } serializeInt64(balance, buffer: buffer, boxed: false) break + case .updateStarsRevenueStatus(let peer, let status): + if boxed { + buffer.appendInt32(-1518030823) + } + peer.serialize(buffer, true) + status.serialize(buffer, true) + break case .updateStickerSets(let flags): if boxed { buffer.appendInt32(834816008) @@ -1621,6 +1647,8 @@ public extension Api { return ("updateBotWebhookJSONQuery", [("queryId", queryId as Any), ("data", data as Any), ("timeout", timeout as Any)]) case .updateBroadcastRevenueTransactions(let peer, let balances): return ("updateBroadcastRevenueTransactions", [("peer", peer as Any), ("balances", balances as Any)]) + case .updateBusinessBotCallbackQuery(let flags, let queryId, let userId, let connectionId, let message, let replyToMessage, let chatInstance, let data): + return ("updateBusinessBotCallbackQuery", [("flags", flags as Any), ("queryId", queryId as Any), ("userId", userId as Any), ("connectionId", connectionId as Any), ("message", message as Any), ("replyToMessage", replyToMessage as Any), ("chatInstance", chatInstance as Any), ("data", data as Any)]) case .updateChannel(let channelId): return ("updateChannel", [("channelId", channelId as Any)]) case .updateChannelAvailableMessages(let channelId, let availableMinId): @@ -1823,6 +1851,8 @@ public extension Api { return ("updateSmsJob", [("jobId", jobId as Any)]) case .updateStarsBalance(let balance): return ("updateStarsBalance", [("balance", balance as Any)]) + case .updateStarsRevenueStatus(let peer, let status): + return ("updateStarsRevenueStatus", [("peer", peer as Any), ("status", status as Any)]) case .updateStickerSets(let flags): return ("updateStickerSets", [("flags", flags as Any)]) case .updateStickerSetsOrder(let flags, let order): @@ -2333,6 +2363,42 @@ public extension Api { return nil } } + public static func parse_updateBusinessBotCallbackQuery(_ reader: BufferReader) -> Update? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int64? + _3 = reader.readInt64() + var _4: String? + _4 = parseString(reader) + var _5: Api.Message? + if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.Message + } + var _6: Api.Message? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.Message + } } + var _7: Int64? + _7 = reader.readInt64() + var _8: Buffer? + if Int(_1!) & Int(1 << 0) != 0 {_8 = parseBytes(reader) } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c7 = _7 != nil + let _c8 = (Int(_1!) & Int(1 << 0) == 0) || _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.Update.updateBusinessBotCallbackQuery(flags: _1!, queryId: _2!, userId: _3!, connectionId: _4!, message: _5!, replyToMessage: _6, chatInstance: _7!, data: _8) + } + else { + return nil + } + } public static func parse_updateChannel(_ reader: BufferReader) -> Update? { var _1: Int64? _1 = reader.readInt64() @@ -3179,9 +3245,9 @@ public extension Api { } var _2: Int32? _2 = reader.readInt32() - var _3: Api.MessageExtendedMedia? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.MessageExtendedMedia + var _3: [Api.MessageExtendedMedia]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageExtendedMedia.self) } let _c1 = _1 != nil let _c2 = _2 != nil @@ -4010,6 +4076,24 @@ public extension Api { return nil } } + public static func parse_updateStarsRevenueStatus(_ reader: BufferReader) -> Update? { + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _2: Api.StarsRevenueStatus? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.StarsRevenueStatus + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.Update.updateStarsRevenueStatus(peer: _1!, status: _2!) + } + else { + return nil + } + } public static func parse_updateStickerSets(_ reader: BufferReader) -> Update? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index 189331107c9..2518361353e 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -160,15 +160,16 @@ public extension Api { } public extension Api { enum WebViewResult: TypeConstructorDescription { - case webViewResultUrl(queryId: Int64, url: String) + case webViewResultUrl(flags: Int32, queryId: Int64?, url: String) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .webViewResultUrl(let queryId, let url): + case .webViewResultUrl(let flags, let queryId, let url): if boxed { - buffer.appendInt32(202659196) + buffer.appendInt32(1294139288) } - serializeInt64(queryId, buffer: buffer, boxed: false) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt64(queryId!, buffer: buffer, boxed: false)} serializeString(url, buffer: buffer, boxed: false) break } @@ -176,20 +177,23 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .webViewResultUrl(let queryId, let url): - return ("webViewResultUrl", [("queryId", queryId as Any), ("url", url as Any)]) + case .webViewResultUrl(let flags, let queryId, let url): + return ("webViewResultUrl", [("flags", flags as Any), ("queryId", queryId as Any), ("url", url as Any)]) } } public static func parse_webViewResultUrl(_ reader: BufferReader) -> WebViewResult? { - var _1: Int64? - _1 = reader.readInt64() - var _2: String? - _2 = parseString(reader) + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + if Int(_1!) & Int(1 << 0) != 0 {_2 = reader.readInt64() } + var _3: String? + _3 = parseString(reader) let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.WebViewResult.webViewResultUrl(queryId: _1!, url: _2!) + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.WebViewResult.webViewResultUrl(flags: _1!, queryId: _2, url: _3!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index a8f5ef4aa74..29cc8fa57a0 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -589,7 +589,7 @@ public extension Api.auth { case sentCodeTypeApp(length: Int32) case sentCodeTypeCall(length: Int32) case sentCodeTypeEmailCode(flags: Int32, emailPattern: String, length: Int32, resetAvailablePeriod: Int32?, resetPendingDate: Int32?) - case sentCodeTypeFirebaseSms(flags: Int32, nonce: Buffer?, playIntegrityNonce: Buffer?, receipt: String?, pushTimeout: Int32?, length: Int32) + case sentCodeTypeFirebaseSms(flags: Int32, nonce: Buffer?, playIntegrityProjectId: Int64?, playIntegrityNonce: Buffer?, receipt: String?, pushTimeout: Int32?, length: Int32) case sentCodeTypeFlashCall(pattern: String) case sentCodeTypeFragmentSms(url: String, length: Int32) case sentCodeTypeMissedCall(prefix: String, length: Int32) @@ -622,12 +622,13 @@ public extension Api.auth { if Int(flags) & Int(1 << 3) != 0 {serializeInt32(resetAvailablePeriod!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 4) != 0 {serializeInt32(resetPendingDate!, buffer: buffer, boxed: false)} break - case .sentCodeTypeFirebaseSms(let flags, let nonce, let playIntegrityNonce, let receipt, let pushTimeout, let length): + case .sentCodeTypeFirebaseSms(let flags, let nonce, let playIntegrityProjectId, let playIntegrityNonce, let receipt, let pushTimeout, let length): if boxed { - buffer.appendInt32(331943703) + buffer.appendInt32(10475318) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeBytes(nonce!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeInt64(playIntegrityProjectId!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 2) != 0 {serializeBytes(playIntegrityNonce!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 1) != 0 {serializeString(receipt!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 1) != 0 {serializeInt32(pushTimeout!, buffer: buffer, boxed: false)} @@ -690,8 +691,8 @@ public extension Api.auth { return ("sentCodeTypeCall", [("length", length as Any)]) case .sentCodeTypeEmailCode(let flags, let emailPattern, let length, let resetAvailablePeriod, let resetPendingDate): return ("sentCodeTypeEmailCode", [("flags", flags as Any), ("emailPattern", emailPattern as Any), ("length", length as Any), ("resetAvailablePeriod", resetAvailablePeriod as Any), ("resetPendingDate", resetPendingDate as Any)]) - case .sentCodeTypeFirebaseSms(let flags, let nonce, let playIntegrityNonce, let receipt, let pushTimeout, let length): - return ("sentCodeTypeFirebaseSms", [("flags", flags as Any), ("nonce", nonce as Any), ("playIntegrityNonce", playIntegrityNonce as Any), ("receipt", receipt as Any), ("pushTimeout", pushTimeout as Any), ("length", length as Any)]) + case .sentCodeTypeFirebaseSms(let flags, let nonce, let playIntegrityProjectId, let playIntegrityNonce, let receipt, let pushTimeout, let length): + return ("sentCodeTypeFirebaseSms", [("flags", flags as Any), ("nonce", nonce as Any), ("playIntegrityProjectId", playIntegrityProjectId as Any), ("playIntegrityNonce", playIntegrityNonce as Any), ("receipt", receipt as Any), ("pushTimeout", pushTimeout as Any), ("length", length as Any)]) case .sentCodeTypeFlashCall(let pattern): return ("sentCodeTypeFlashCall", [("pattern", pattern as Any)]) case .sentCodeTypeFragmentSms(let url, let length): @@ -759,22 +760,25 @@ public extension Api.auth { _1 = reader.readInt32() var _2: Buffer? if Int(_1!) & Int(1 << 0) != 0 {_2 = parseBytes(reader) } - var _3: Buffer? - if Int(_1!) & Int(1 << 2) != 0 {_3 = parseBytes(reader) } - var _4: String? - if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } - var _5: Int32? - if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() } + var _3: Int64? + if Int(_1!) & Int(1 << 2) != 0 {_3 = reader.readInt64() } + var _4: Buffer? + if Int(_1!) & Int(1 << 2) != 0 {_4 = parseBytes(reader) } + var _5: String? + if Int(_1!) & Int(1 << 1) != 0 {_5 = parseString(reader) } var _6: Int32? - _6 = reader.readInt32() + if Int(_1!) & Int(1 << 1) != 0 {_6 = reader.readInt32() } + var _7: Int32? + _7 = reader.readInt32() let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.auth.SentCodeType.sentCodeTypeFirebaseSms(flags: _1!, nonce: _2, playIntegrityNonce: _3, receipt: _4, pushTimeout: _5, length: _6!) + let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.auth.SentCodeType.sentCodeTypeFirebaseSms(flags: _1!, nonce: _2, playIntegrityProjectId: _3, playIntegrityNonce: _4, receipt: _5, pushTimeout: _6, length: _7!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index 3ec0b4d752c..331ac6b496a 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -1,53 +1,3 @@ -public extension Api { - enum BusinessIntro: TypeConstructorDescription { - case businessIntro(flags: Int32, title: String, description: String, sticker: Api.Document?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .businessIntro(let flags, let title, let description, let sticker): - if boxed { - buffer.appendInt32(1510606445) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) - serializeString(description, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {sticker!.serialize(buffer, true)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .businessIntro(let flags, let title, let description, let sticker): - return ("businessIntro", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("sticker", sticker as Any)]) - } - } - - public static func parse_businessIntro(_ reader: BufferReader) -> BusinessIntro? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: String? - _3 = parseString(reader) - var _4: Api.Document? - if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.Document - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.BusinessIntro.businessIntro(flags: _1!, title: _2!, description: _3!, sticker: _4) - } - else { - return nil - } - } - - } -} public extension Api { enum BusinessLocation: TypeConstructorDescription { case businessLocation(flags: Int32, geoPoint: Api.GeoPoint?, address: String) diff --git a/submodules/TelegramApi/Sources/Api33.swift b/submodules/TelegramApi/Sources/Api33.swift index da4798d2eef..b8429bdcdcc 100644 --- a/submodules/TelegramApi/Sources/Api33.swift +++ b/submodules/TelegramApi/Sources/Api33.swift @@ -1044,6 +1044,126 @@ public extension Api.payments { } } +public extension Api.payments { + enum StarsRevenueAdsAccountUrl: TypeConstructorDescription { + case starsRevenueAdsAccountUrl(url: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsRevenueAdsAccountUrl(let url): + if boxed { + buffer.appendInt32(961445665) + } + serializeString(url, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starsRevenueAdsAccountUrl(let url): + return ("starsRevenueAdsAccountUrl", [("url", url as Any)]) + } + } + + public static func parse_starsRevenueAdsAccountUrl(_ reader: BufferReader) -> StarsRevenueAdsAccountUrl? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.payments.StarsRevenueAdsAccountUrl.starsRevenueAdsAccountUrl(url: _1!) + } + else { + return nil + } + } + + } +} +public extension Api.payments { + enum StarsRevenueStats: TypeConstructorDescription { + case starsRevenueStats(revenueGraph: Api.StatsGraph, status: Api.StarsRevenueStatus, usdRate: Double) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsRevenueStats(let revenueGraph, let status, let usdRate): + if boxed { + buffer.appendInt32(-919881925) + } + revenueGraph.serialize(buffer, true) + status.serialize(buffer, true) + serializeDouble(usdRate, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starsRevenueStats(let revenueGraph, let status, let usdRate): + return ("starsRevenueStats", [("revenueGraph", revenueGraph as Any), ("status", status as Any), ("usdRate", usdRate as Any)]) + } + } + + public static func parse_starsRevenueStats(_ reader: BufferReader) -> StarsRevenueStats? { + var _1: Api.StatsGraph? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _2: Api.StarsRevenueStatus? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.StarsRevenueStatus + } + var _3: Double? + _3 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.payments.StarsRevenueStats.starsRevenueStats(revenueGraph: _1!, status: _2!, usdRate: _3!) + } + else { + return nil + } + } + + } +} +public extension Api.payments { + enum StarsRevenueWithdrawalUrl: TypeConstructorDescription { + case starsRevenueWithdrawalUrl(url: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsRevenueWithdrawalUrl(let url): + if boxed { + buffer.appendInt32(497778871) + } + serializeString(url, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starsRevenueWithdrawalUrl(let url): + return ("starsRevenueWithdrawalUrl", [("url", url as Any)]) + } + } + + public static func parse_starsRevenueWithdrawalUrl(_ reader: BufferReader) -> StarsRevenueWithdrawalUrl? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.payments.StarsRevenueWithdrawalUrl.starsRevenueWithdrawalUrl(url: _1!) + } + else { + return nil + } + } + + } +} public extension Api.payments { enum StarsStatus: TypeConstructorDescription { case starsStatus(flags: Int32, balance: Int64, history: [Api.StarsTransaction], nextOffset: String?, chats: [Api.Chat], users: [Api.User]) @@ -1542,143 +1662,3 @@ public extension Api.phone { } } -public extension Api.photos { - enum Photo: TypeConstructorDescription { - case photo(photo: Api.Photo, users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .photo(let photo, let users): - if boxed { - buffer.appendInt32(539045032) - } - photo.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .photo(let photo, let users): - return ("photo", [("photo", photo as Any), ("users", users as Any)]) - } - } - - public static func parse_photo(_ reader: BufferReader) -> Photo? { - var _1: Api.Photo? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.Photo - } - var _2: [Api.User]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.photos.Photo.photo(photo: _1!, users: _2!) - } - else { - return nil - } - } - - } -} -public extension Api.photos { - enum Photos: TypeConstructorDescription { - case photos(photos: [Api.Photo], users: [Api.User]) - case photosSlice(count: Int32, photos: [Api.Photo], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .photos(let photos, let users): - if boxed { - buffer.appendInt32(-1916114267) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(photos.count)) - for item in photos { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - case .photosSlice(let count, let photos, let users): - if boxed { - buffer.appendInt32(352657236) - } - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(photos.count)) - for item in photos { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .photos(let photos, let users): - return ("photos", [("photos", photos as Any), ("users", users as Any)]) - case .photosSlice(let count, let photos, let users): - return ("photosSlice", [("count", count as Any), ("photos", photos as Any), ("users", users as Any)]) - } - } - - public static func parse_photos(_ reader: BufferReader) -> Photos? { - var _1: [Api.Photo]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Photo.self) - } - var _2: [Api.User]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.photos.Photos.photos(photos: _1!, users: _2!) - } - else { - return nil - } - } - public static func parse_photosSlice(_ reader: BufferReader) -> Photos? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.Photo]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Photo.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.photos.Photos.photosSlice(count: _1!, photos: _2!, users: _3!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api34.swift b/submodules/TelegramApi/Sources/Api34.swift index 8952189c1c0..35e27005456 100644 --- a/submodules/TelegramApi/Sources/Api34.swift +++ b/submodules/TelegramApi/Sources/Api34.swift @@ -1,3 +1,143 @@ +public extension Api.photos { + enum Photo: TypeConstructorDescription { + case photo(photo: Api.Photo, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .photo(let photo, let users): + if boxed { + buffer.appendInt32(539045032) + } + photo.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .photo(let photo, let users): + return ("photo", [("photo", photo as Any), ("users", users as Any)]) + } + } + + public static func parse_photo(_ reader: BufferReader) -> Photo? { + var _1: Api.Photo? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Photo + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.photos.Photo.photo(photo: _1!, users: _2!) + } + else { + return nil + } + } + + } +} +public extension Api.photos { + enum Photos: TypeConstructorDescription { + case photos(photos: [Api.Photo], users: [Api.User]) + case photosSlice(count: Int32, photos: [Api.Photo], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .photos(let photos, let users): + if boxed { + buffer.appendInt32(-1916114267) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(photos.count)) + for item in photos { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + case .photosSlice(let count, let photos, let users): + if boxed { + buffer.appendInt32(352657236) + } + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(photos.count)) + for item in photos { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .photos(let photos, let users): + return ("photos", [("photos", photos as Any), ("users", users as Any)]) + case .photosSlice(let count, let photos, let users): + return ("photosSlice", [("count", count as Any), ("photos", photos as Any), ("users", users as Any)]) + } + } + + public static func parse_photos(_ reader: BufferReader) -> Photos? { + var _1: [Api.Photo]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Photo.self) + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.photos.Photos.photos(photos: _1!, users: _2!) + } + else { + return nil + } + } + public static func parse_photosSlice(_ reader: BufferReader) -> Photos? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.Photo]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Photo.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.photos.Photos.photosSlice(count: _1!, photos: _2!, users: _3!) + } + else { + return nil + } + } + + } +} public extension Api.premium { enum BoostsList: TypeConstructorDescription { case boostsList(flags: Int32, count: Int32, boosts: [Api.Boost], nextOffset: String?, users: [Api.User]) @@ -1213,72 +1353,14 @@ public extension Api.stories { } } public extension Api.stories { - enum PeerStories: TypeConstructorDescription { - case peerStories(stories: Api.PeerStories, chats: [Api.Chat], users: [Api.User]) + enum FoundStories: TypeConstructorDescription { + case foundStories(flags: Int32, count: Int32, stories: [Api.FoundStory], nextOffset: String?, chats: [Api.Chat], users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .peerStories(let stories, let chats, let users): + case .foundStories(let flags, let count, let stories, let nextOffset, let chats, let users): if boxed { - buffer.appendInt32(-890861720) - } - stories.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .peerStories(let stories, let chats, let users): - return ("peerStories", [("stories", stories as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_peerStories(_ reader: BufferReader) -> PeerStories? { - var _1: Api.PeerStories? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.PeerStories - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.stories.PeerStories.peerStories(stories: _1!, chats: _2!, users: _3!) - } - else { - return nil - } - } - - } -} -public extension Api.stories { - enum Stories: TypeConstructorDescription { - case stories(flags: Int32, count: Int32, stories: [Api.StoryItem], pinnedToTop: [Int32]?, chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .stories(let flags, let count, let stories, let pinnedToTop, let chats, let users): - if boxed { - buffer.appendInt32(1673780490) + buffer.appendInt32(-488736969) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(count, buffer: buffer, boxed: false) @@ -1287,11 +1369,7 @@ public extension Api.stories { for item in stories { item.serialize(buffer, true) } - if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(pinnedToTop!.count)) - for item in pinnedToTop! { - serializeInt32(item, buffer: buffer, boxed: false) - }} + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) buffer.appendInt32(Int32(chats.count)) for item in chats { @@ -1308,24 +1386,22 @@ public extension Api.stories { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .stories(let flags, let count, let stories, let pinnedToTop, let chats, let users): - return ("stories", [("flags", flags as Any), ("count", count as Any), ("stories", stories as Any), ("pinnedToTop", pinnedToTop as Any), ("chats", chats as Any), ("users", users as Any)]) + case .foundStories(let flags, let count, let stories, let nextOffset, let chats, let users): + return ("foundStories", [("flags", flags as Any), ("count", count as Any), ("stories", stories as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any)]) } } - public static func parse_stories(_ reader: BufferReader) -> Stories? { + public static func parse_foundStories(_ reader: BufferReader) -> FoundStories? { var _1: Int32? _1 = reader.readInt32() var _2: Int32? _2 = reader.readInt32() - var _3: [Api.StoryItem]? + var _3: [Api.FoundStory]? if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryItem.self) + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.FoundStory.self) } - var _4: [Int32]? - if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) - } } + var _4: String? + if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) } var _5: [Api.Chat]? if let _ = reader.readInt32() { _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) @@ -1341,7 +1417,7 @@ public extension Api.stories { let _c5 = _5 != nil let _c6 = _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.stories.Stories.stories(flags: _1!, count: _2!, stories: _3!, pinnedToTop: _4, chats: _5!, users: _6!) + return Api.stories.FoundStories.foundStories(flags: _1!, count: _2!, stories: _3!, nextOffset: _4, chats: _5!, users: _6!) } else { return nil @@ -1351,22 +1427,16 @@ public extension Api.stories { } } public extension Api.stories { - enum StoryReactionsList: TypeConstructorDescription { - case storyReactionsList(flags: Int32, count: Int32, reactions: [Api.StoryReaction], chats: [Api.Chat], users: [Api.User], nextOffset: String?) + enum PeerStories: TypeConstructorDescription { + case peerStories(stories: Api.PeerStories, chats: [Api.Chat], users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset): + case .peerStories(let stories, let chats, let users): if boxed { - buffer.appendInt32(-1436583780) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(reactions.count)) - for item in reactions { - item.serialize(buffer, true) + buffer.appendInt32(-890861720) } + stories.serialize(buffer, true) buffer.appendInt32(481674261) buffer.appendInt32(Int32(chats.count)) for item in chats { @@ -1377,97 +1447,35 @@ public extension Api.stories { for item in users { item.serialize(buffer, true) } - if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset): - return ("storyReactionsList", [("flags", flags as Any), ("count", count as Any), ("reactions", reactions as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) + case .peerStories(let stories, let chats, let users): + return ("peerStories", [("stories", stories as Any), ("chats", chats as Any), ("users", users as Any)]) } } - public static func parse_storyReactionsList(_ reader: BufferReader) -> StoryReactionsList? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: [Api.StoryReaction]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryReaction.self) + public static func parse_peerStories(_ reader: BufferReader) -> PeerStories? { + var _1: Api.PeerStories? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.PeerStories } - var _4: [Api.Chat]? + var _2: [Api.Chat]? if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) } - var _5: [Api.User]? + var _3: [Api.User]? if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } - var _6: String? - if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.stories.StoryReactionsList.storyReactionsList(flags: _1!, count: _2!, reactions: _3!, chats: _4!, users: _5!, nextOffset: _6) - } - else { - return nil - } - } - - } -} -public extension Api.stories { - enum StoryViews: TypeConstructorDescription { - case storyViews(views: [Api.StoryViews], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .storyViews(let views, let users): - if boxed { - buffer.appendInt32(-560009955) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(views.count)) - for item in views { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .storyViews(let views, let users): - return ("storyViews", [("views", views as Any), ("users", users as Any)]) - } - } - - public static func parse_storyViews(_ reader: BufferReader) -> StoryViews? { - var _1: [Api.StoryViews]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryViews.self) - } - var _2: [Api.User]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.stories.StoryViews.storyViews(views: _1!, users: _2!) + if _c1 && _c2 && _c3 { + return Api.stories.PeerStories.peerStories(stories: _1!, chats: _2!, users: _3!) } else { return nil @@ -1477,147 +1485,27 @@ public extension Api.stories { } } public extension Api.stories { - enum StoryViewsList: TypeConstructorDescription { - case storyViewsList(flags: Int32, count: Int32, viewsCount: Int32, forwardsCount: Int32, reactionsCount: Int32, views: [Api.StoryView], chats: [Api.Chat], users: [Api.User], nextOffset: String?) + enum Stories: TypeConstructorDescription { + case stories(flags: Int32, count: Int32, stories: [Api.StoryItem], pinnedToTop: [Int32]?, chats: [Api.Chat], users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .storyViewsList(let flags, let count, let viewsCount, let forwardsCount, let reactionsCount, let views, let chats, let users, let nextOffset): + case .stories(let flags, let count, let stories, let pinnedToTop, let chats, let users): if boxed { - buffer.appendInt32(1507299269) + buffer.appendInt32(1673780490) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(count, buffer: buffer, boxed: false) - serializeInt32(viewsCount, buffer: buffer, boxed: false) - serializeInt32(forwardsCount, buffer: buffer, boxed: false) - serializeInt32(reactionsCount, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(views.count)) - for item in views { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .storyViewsList(let flags, let count, let viewsCount, let forwardsCount, let reactionsCount, let views, let chats, let users, let nextOffset): - return ("storyViewsList", [("flags", flags as Any), ("count", count as Any), ("viewsCount", viewsCount as Any), ("forwardsCount", forwardsCount as Any), ("reactionsCount", reactionsCount as Any), ("views", views as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) - } - } - - public static func parse_storyViewsList(_ reader: BufferReader) -> StoryViewsList? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int32? - _5 = reader.readInt32() - var _6: [Api.StoryView]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryView.self) - } - var _7: [Api.Chat]? - if let _ = reader.readInt32() { - _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _8: [Api.User]? - if let _ = reader.readInt32() { - _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - var _9: String? - if Int(_1!) & Int(1 << 0) != 0 {_9 = parseString(reader) } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 0) == 0) || _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.stories.StoryViewsList.storyViewsList(flags: _1!, count: _2!, viewsCount: _3!, forwardsCount: _4!, reactionsCount: _5!, views: _6!, chats: _7!, users: _8!, nextOffset: _9) - } - else { - return nil - } - } - - } -} -public extension Api.updates { - indirect enum ChannelDifference: TypeConstructorDescription { - case channelDifference(flags: Int32, pts: Int32, timeout: Int32?, newMessages: [Api.Message], otherUpdates: [Api.Update], chats: [Api.Chat], users: [Api.User]) - case channelDifferenceEmpty(flags: Int32, pts: Int32, timeout: Int32?) - case channelDifferenceTooLong(flags: Int32, timeout: Int32?, dialog: Api.Dialog, messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .channelDifference(let flags, let pts, let timeout, let newMessages, let otherUpdates, let chats, let users): - if boxed { - buffer.appendInt32(543450958) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(pts, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {serializeInt32(timeout!, buffer: buffer, boxed: false)} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(newMessages.count)) - for item in newMessages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(otherUpdates.count)) - for item in otherUpdates { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - case .channelDifferenceEmpty(let flags, let pts, let timeout): - if boxed { - buffer.appendInt32(1041346555) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(pts, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {serializeInt32(timeout!, buffer: buffer, boxed: false)} - break - case .channelDifferenceTooLong(let flags, let timeout, let dialog, let messages, let chats, let users): - if boxed { - buffer.appendInt32(-1531132162) - } - serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {serializeInt32(timeout!, buffer: buffer, boxed: false)} - dialog.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messages.count)) - for item in messages { + buffer.appendInt32(Int32(stories.count)) + for item in stories { item.serialize(buffer, true) } + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(pinnedToTop!.count)) + for item in pinnedToTop! { + serializeInt32(item, buffer: buffer, boxed: false) + }} buffer.appendInt32(481674261) buffer.appendInt32(Int32(chats.count)) for item in chats { @@ -1634,82 +1522,24 @@ public extension Api.updates { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .channelDifference(let flags, let pts, let timeout, let newMessages, let otherUpdates, let chats, let users): - return ("channelDifference", [("flags", flags as Any), ("pts", pts as Any), ("timeout", timeout as Any), ("newMessages", newMessages as Any), ("otherUpdates", otherUpdates as Any), ("chats", chats as Any), ("users", users as Any)]) - case .channelDifferenceEmpty(let flags, let pts, let timeout): - return ("channelDifferenceEmpty", [("flags", flags as Any), ("pts", pts as Any), ("timeout", timeout as Any)]) - case .channelDifferenceTooLong(let flags, let timeout, let dialog, let messages, let chats, let users): - return ("channelDifferenceTooLong", [("flags", flags as Any), ("timeout", timeout as Any), ("dialog", dialog as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) + case .stories(let flags, let count, let stories, let pinnedToTop, let chats, let users): + return ("stories", [("flags", flags as Any), ("count", count as Any), ("stories", stories as Any), ("pinnedToTop", pinnedToTop as Any), ("chats", chats as Any), ("users", users as Any)]) } } - public static func parse_channelDifference(_ reader: BufferReader) -> ChannelDifference? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - if Int(_1!) & Int(1 << 1) != 0 {_3 = reader.readInt32() } - var _4: [Api.Message]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _5: [Api.Update]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Update.self) - } - var _6: [Api.Chat]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _7: [Api.User]? - if let _ = reader.readInt32() { - _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.updates.ChannelDifference.channelDifference(flags: _1!, pts: _2!, timeout: _3, newMessages: _4!, otherUpdates: _5!, chats: _6!, users: _7!) - } - else { - return nil - } - } - public static func parse_channelDifferenceEmpty(_ reader: BufferReader) -> ChannelDifference? { + public static func parse_stories(_ reader: BufferReader) -> Stories? { var _1: Int32? _1 = reader.readInt32() var _2: Int32? _2 = reader.readInt32() - var _3: Int32? - if Int(_1!) & Int(1 << 1) != 0 {_3 = reader.readInt32() } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - if _c1 && _c2 && _c3 { - return Api.updates.ChannelDifference.channelDifferenceEmpty(flags: _1!, pts: _2!, timeout: _3) - } - else { - return nil - } - } - public static func parse_channelDifferenceTooLong(_ reader: BufferReader) -> ChannelDifference? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - if Int(_1!) & Int(1 << 1) != 0 {_2 = reader.readInt32() } - var _3: Api.Dialog? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.Dialog - } - var _4: [Api.Message]? + var _3: [Api.StoryItem]? if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryItem.self) } + var _4: [Int32]? + if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } } var _5: [Api.Chat]? if let _ = reader.readInt32() { _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) @@ -1719,13 +1549,13 @@ public extension Api.updates { _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil + let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = _4 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.updates.ChannelDifference.channelDifferenceTooLong(flags: _1!, timeout: _2, dialog: _3!, messages: _4!, chats: _5!, users: _6!) + return Api.stories.Stories.stories(flags: _1!, count: _2!, stories: _3!, pinnedToTop: _4, chats: _5!, users: _6!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api35.swift b/submodules/TelegramApi/Sources/Api35.swift index f6d0dc14fb8..c3cabdd4c7e 100644 --- a/submodules/TelegramApi/Sources/Api35.swift +++ b/submodules/TelegramApi/Sources/Api35.swift @@ -1,3 +1,387 @@ +public extension Api.stories { + enum StoryReactionsList: TypeConstructorDescription { + case storyReactionsList(flags: Int32, count: Int32, reactions: [Api.StoryReaction], chats: [Api.Chat], users: [Api.User], nextOffset: String?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset): + if boxed { + buffer.appendInt32(-1436583780) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(reactions.count)) + for item in reactions { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset): + return ("storyReactionsList", [("flags", flags as Any), ("count", count as Any), ("reactions", reactions as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) + } + } + + public static func parse_storyReactionsList(_ reader: BufferReader) -> StoryReactionsList? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.StoryReaction]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryReaction.self) + } + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _6: String? + if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.stories.StoryReactionsList.storyReactionsList(flags: _1!, count: _2!, reactions: _3!, chats: _4!, users: _5!, nextOffset: _6) + } + else { + return nil + } + } + + } +} +public extension Api.stories { + enum StoryViews: TypeConstructorDescription { + case storyViews(views: [Api.StoryViews], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .storyViews(let views, let users): + if boxed { + buffer.appendInt32(-560009955) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(views.count)) + for item in views { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .storyViews(let views, let users): + return ("storyViews", [("views", views as Any), ("users", users as Any)]) + } + } + + public static func parse_storyViews(_ reader: BufferReader) -> StoryViews? { + var _1: [Api.StoryViews]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryViews.self) + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.stories.StoryViews.storyViews(views: _1!, users: _2!) + } + else { + return nil + } + } + + } +} +public extension Api.stories { + enum StoryViewsList: TypeConstructorDescription { + case storyViewsList(flags: Int32, count: Int32, viewsCount: Int32, forwardsCount: Int32, reactionsCount: Int32, views: [Api.StoryView], chats: [Api.Chat], users: [Api.User], nextOffset: String?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .storyViewsList(let flags, let count, let viewsCount, let forwardsCount, let reactionsCount, let views, let chats, let users, let nextOffset): + if boxed { + buffer.appendInt32(1507299269) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + serializeInt32(viewsCount, buffer: buffer, boxed: false) + serializeInt32(forwardsCount, buffer: buffer, boxed: false) + serializeInt32(reactionsCount, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(views.count)) + for item in views { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .storyViewsList(let flags, let count, let viewsCount, let forwardsCount, let reactionsCount, let views, let chats, let users, let nextOffset): + return ("storyViewsList", [("flags", flags as Any), ("count", count as Any), ("viewsCount", viewsCount as Any), ("forwardsCount", forwardsCount as Any), ("reactionsCount", reactionsCount as Any), ("views", views as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) + } + } + + public static func parse_storyViewsList(_ reader: BufferReader) -> StoryViewsList? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: [Api.StoryView]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryView.self) + } + var _7: [Api.Chat]? + if let _ = reader.readInt32() { + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _8: [Api.User]? + if let _ = reader.readInt32() { + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _9: String? + if Int(_1!) & Int(1 << 0) != 0 {_9 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = (Int(_1!) & Int(1 << 0) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.stories.StoryViewsList.storyViewsList(flags: _1!, count: _2!, viewsCount: _3!, forwardsCount: _4!, reactionsCount: _5!, views: _6!, chats: _7!, users: _8!, nextOffset: _9) + } + else { + return nil + } + } + + } +} +public extension Api.updates { + indirect enum ChannelDifference: TypeConstructorDescription { + case channelDifference(flags: Int32, pts: Int32, timeout: Int32?, newMessages: [Api.Message], otherUpdates: [Api.Update], chats: [Api.Chat], users: [Api.User]) + case channelDifferenceEmpty(flags: Int32, pts: Int32, timeout: Int32?) + case channelDifferenceTooLong(flags: Int32, timeout: Int32?, dialog: Api.Dialog, messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .channelDifference(let flags, let pts, let timeout, let newMessages, let otherUpdates, let chats, let users): + if boxed { + buffer.appendInt32(543450958) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(pts, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(timeout!, buffer: buffer, boxed: false)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(newMessages.count)) + for item in newMessages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(otherUpdates.count)) + for item in otherUpdates { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + case .channelDifferenceEmpty(let flags, let pts, let timeout): + if boxed { + buffer.appendInt32(1041346555) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(pts, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(timeout!, buffer: buffer, boxed: false)} + break + case .channelDifferenceTooLong(let flags, let timeout, let dialog, let messages, let chats, let users): + if boxed { + buffer.appendInt32(-1531132162) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(timeout!, buffer: buffer, boxed: false)} + dialog.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .channelDifference(let flags, let pts, let timeout, let newMessages, let otherUpdates, let chats, let users): + return ("channelDifference", [("flags", flags as Any), ("pts", pts as Any), ("timeout", timeout as Any), ("newMessages", newMessages as Any), ("otherUpdates", otherUpdates as Any), ("chats", chats as Any), ("users", users as Any)]) + case .channelDifferenceEmpty(let flags, let pts, let timeout): + return ("channelDifferenceEmpty", [("flags", flags as Any), ("pts", pts as Any), ("timeout", timeout as Any)]) + case .channelDifferenceTooLong(let flags, let timeout, let dialog, let messages, let chats, let users): + return ("channelDifferenceTooLong", [("flags", flags as Any), ("timeout", timeout as Any), ("dialog", dialog as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_channelDifference(_ reader: BufferReader) -> ChannelDifference? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_3 = reader.readInt32() } + var _4: [Api.Message]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _5: [Api.Update]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Update.self) + } + var _6: [Api.Chat]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _7: [Api.User]? + if let _ = reader.readInt32() { + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.updates.ChannelDifference.channelDifference(flags: _1!, pts: _2!, timeout: _3, newMessages: _4!, otherUpdates: _5!, chats: _6!, users: _7!) + } + else { + return nil + } + } + public static func parse_channelDifferenceEmpty(_ reader: BufferReader) -> ChannelDifference? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_3 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.updates.ChannelDifference.channelDifferenceEmpty(flags: _1!, pts: _2!, timeout: _3) + } + else { + return nil + } + } + public static func parse_channelDifferenceTooLong(_ reader: BufferReader) -> ChannelDifference? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_2 = reader.readInt32() } + var _3: Api.Dialog? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.Dialog + } + var _4: [Api.Message]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _5: [Api.Chat]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _6: [Api.User]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.updates.ChannelDifference.channelDifferenceTooLong(flags: _1!, timeout: _2, dialog: _3!, messages: _4!, chats: _5!, users: _6!) + } + else { + return nil + } + } + + } +} public extension Api.updates { enum Difference: TypeConstructorDescription { case difference(newMessages: [Api.Message], newEncryptedMessages: [Api.EncryptedMessage], otherUpdates: [Api.Update], chats: [Api.Chat], users: [Api.User], state: Api.updates.State) diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index 283bdcd1dc7..e2e23ca711e 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -7308,20 +7308,20 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func requestAppWebView(flags: Int32, peer: Api.InputPeer, app: Api.InputBotApp, startParam: String?, themeParams: Api.DataJSON?, platform: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func requestAppWebView(flags: Int32, peer: Api.InputPeer, app: Api.InputBotApp, startParam: String?, themeParams: Api.DataJSON?, platform: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1940243652) + buffer.appendInt32(1398901710) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) app.serialize(buffer, true) if Int(flags) & Int(1 << 1) != 0 {serializeString(startParam!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 2) != 0 {themeParams!.serialize(buffer, true)} serializeString(platform, buffer: buffer, boxed: false) - return (FunctionDescription(name: "messages.requestAppWebView", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("app", String(describing: app)), ("startParam", String(describing: startParam)), ("themeParams", String(describing: themeParams)), ("platform", String(describing: platform))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.AppWebViewResult? in + return (FunctionDescription(name: "messages.requestAppWebView", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("app", String(describing: app)), ("startParam", String(describing: startParam)), ("themeParams", String(describing: themeParams)), ("platform", String(describing: platform))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.WebViewResult? in let reader = BufferReader(buffer) - var result: Api.AppWebViewResult? + var result: Api.WebViewResult? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.AppWebViewResult + result = Api.parse(reader, signature: signature) as? Api.WebViewResult } return result }) @@ -7345,20 +7345,20 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func requestSimpleWebView(flags: Int32, bot: Api.InputUser, url: String?, startParam: String?, themeParams: Api.DataJSON?, platform: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func requestSimpleWebView(flags: Int32, bot: Api.InputUser, url: String?, startParam: String?, themeParams: Api.DataJSON?, platform: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(440815626) + buffer.appendInt32(1094336115) serializeInt32(flags, buffer: buffer, boxed: false) bot.serialize(buffer, true) if Int(flags) & Int(1 << 3) != 0 {serializeString(url!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 4) != 0 {serializeString(startParam!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {themeParams!.serialize(buffer, true)} serializeString(platform, buffer: buffer, boxed: false) - return (FunctionDescription(name: "messages.requestSimpleWebView", parameters: [("flags", String(describing: flags)), ("bot", String(describing: bot)), ("url", String(describing: url)), ("startParam", String(describing: startParam)), ("themeParams", String(describing: themeParams)), ("platform", String(describing: platform))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.SimpleWebViewResult? in + return (FunctionDescription(name: "messages.requestSimpleWebView", parameters: [("flags", String(describing: flags)), ("bot", String(describing: bot)), ("url", String(describing: url)), ("startParam", String(describing: startParam)), ("themeParams", String(describing: themeParams)), ("platform", String(describing: platform))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.WebViewResult? in let reader = BufferReader(buffer) - var result: Api.SimpleWebViewResult? + var result: Api.WebViewResult? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.SimpleWebViewResult + result = Api.parse(reader, signature: signature) as? Api.WebViewResult } return result }) @@ -7423,9 +7423,9 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func saveDraft(flags: Int32, replyTo: Api.InputReplyTo?, peer: Api.InputPeer, message: String, entities: [Api.MessageEntity]?, media: Api.InputMedia?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func saveDraft(flags: Int32, replyTo: Api.InputReplyTo?, peer: Api.InputPeer, message: String, entities: [Api.MessageEntity]?, media: Api.InputMedia?, effect: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(2146678790) + buffer.appendInt32(-747452978) serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 4) != 0 {replyTo!.serialize(buffer, true)} peer.serialize(buffer, true) @@ -7436,7 +7436,8 @@ public extension Api.functions.messages { item.serialize(buffer, true) }} if Int(flags) & Int(1 << 5) != 0 {media!.serialize(buffer, true)} - return (FunctionDescription(name: "messages.saveDraft", parameters: [("flags", String(describing: flags)), ("replyTo", String(describing: replyTo)), ("peer", String(describing: peer)), ("message", String(describing: message)), ("entities", String(describing: entities)), ("media", String(describing: media))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + if Int(flags) & Int(1 << 7) != 0 {serializeInt64(effect!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "messages.saveDraft", parameters: [("flags", String(describing: flags)), ("replyTo", String(describing: replyTo)), ("peer", String(describing: peer)), ("message", String(describing: message)), ("entities", String(describing: entities)), ("media", String(describing: media)), ("effect", String(describing: effect))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in let reader = BufferReader(buffer) var result: Api.Bool? if let signature = reader.readInt32() { @@ -8730,6 +8731,54 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func getStarsRevenueAdsAccountUrl(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-774377531) + peer.serialize(buffer, true) + return (FunctionDescription(name: "payments.getStarsRevenueAdsAccountUrl", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.StarsRevenueAdsAccountUrl? in + let reader = BufferReader(buffer) + var result: Api.payments.StarsRevenueAdsAccountUrl? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.payments.StarsRevenueAdsAccountUrl + } + return result + }) + } +} +public extension Api.functions.payments { + static func getStarsRevenueStats(flags: Int32, peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-652215594) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + return (FunctionDescription(name: "payments.getStarsRevenueStats", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.StarsRevenueStats? in + let reader = BufferReader(buffer) + var result: Api.payments.StarsRevenueStats? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.payments.StarsRevenueStats + } + return result + }) + } +} +public extension Api.functions.payments { + static func getStarsRevenueWithdrawalUrl(peer: Api.InputPeer, stars: Int64, password: Api.InputCheckPasswordSRP) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(331081907) + peer.serialize(buffer, true) + serializeInt64(stars, buffer: buffer, boxed: false) + password.serialize(buffer, true) + return (FunctionDescription(name: "payments.getStarsRevenueWithdrawalUrl", parameters: [("peer", String(describing: peer)), ("stars", String(describing: stars)), ("password", String(describing: password))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.StarsRevenueWithdrawalUrl? in + let reader = BufferReader(buffer) + var result: Api.payments.StarsRevenueWithdrawalUrl? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.payments.StarsRevenueWithdrawalUrl + } + return result + }) + } +} public extension Api.functions.payments { static func getStarsStatus(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8761,13 +8810,34 @@ public extension Api.functions.payments { } } public extension Api.functions.payments { - static func getStarsTransactions(flags: Int32, peer: Api.InputPeer, offset: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getStarsTransactions(flags: Int32, peer: Api.InputPeer, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1731904249) + buffer.appendInt32(-1751937702) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) serializeString(offset, buffer: buffer, boxed: false) - return (FunctionDescription(name: "payments.getStarsTransactions", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("offset", String(describing: offset))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.StarsStatus? in + serializeInt32(limit, buffer: buffer, boxed: false) + return (FunctionDescription(name: "payments.getStarsTransactions", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.StarsStatus? in + let reader = BufferReader(buffer) + var result: Api.payments.StarsStatus? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.payments.StarsStatus + } + return result + }) + } +} +public extension Api.functions.payments { + static func getStarsTransactionsByID(peer: Api.InputPeer, id: [Api.InputStarsTransaction]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(662973742) + peer.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(id.count)) + for item in id { + item.serialize(buffer, true) + } + return (FunctionDescription(name: "payments.getStarsTransactionsByID", parameters: [("peer", String(describing: peer)), ("id", String(describing: id))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.StarsStatus? in let reader = BufferReader(buffer) var result: Api.payments.StarsStatus? if let signature = reader.readInt32() { @@ -8795,13 +8865,12 @@ public extension Api.functions.payments { } } public extension Api.functions.payments { - static func refundStarsCharge(userId: Api.InputUser, msgId: Int32, chargeId: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func refundStarsCharge(userId: Api.InputUser, chargeId: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-258950164) + buffer.appendInt32(632196938) userId.serialize(buffer, true) - serializeInt32(msgId, buffer: buffer, boxed: false) serializeString(chargeId, buffer: buffer, boxed: false) - return (FunctionDescription(name: "payments.refundStarsCharge", parameters: [("userId", String(describing: userId)), ("msgId", String(describing: msgId)), ("chargeId", String(describing: chargeId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + return (FunctionDescription(name: "payments.refundStarsCharge", parameters: [("userId", String(describing: userId)), ("chargeId", String(describing: chargeId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -10393,6 +10462,25 @@ public extension Api.functions.stories { }) } } +public extension Api.functions.stories { + static func searchPosts(flags: Int32, hashtag: String?, area: Api.MediaArea?, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1827279210) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(hashtag!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {area!.serialize(buffer, true)} + serializeString(offset, buffer: buffer, boxed: false) + serializeInt32(limit, buffer: buffer, boxed: false) + return (FunctionDescription(name: "stories.searchPosts", parameters: [("flags", String(describing: flags)), ("hashtag", String(describing: hashtag)), ("area", String(describing: area)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.FoundStories? in + let reader = BufferReader(buffer) + var result: Api.stories.FoundStories? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.stories.FoundStories + } + return result + }) + } +} public extension Api.functions.stories { static func sendReaction(flags: Int32, peer: Api.InputPeer, storyId: Int32, reaction: Api.Reaction) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramApi/Sources/Api6.swift b/submodules/TelegramApi/Sources/Api6.swift index e3677b92d23..177e11efdea 100644 --- a/submodules/TelegramApi/Sources/Api6.swift +++ b/submodules/TelegramApi/Sources/Api6.swift @@ -1,13 +1,13 @@ public extension Api { indirect enum DraftMessage: TypeConstructorDescription { - case draftMessage(flags: Int32, replyTo: Api.InputReplyTo?, message: String, entities: [Api.MessageEntity]?, media: Api.InputMedia?, date: Int32) + case draftMessage(flags: Int32, replyTo: Api.InputReplyTo?, message: String, entities: [Api.MessageEntity]?, media: Api.InputMedia?, date: Int32, effect: Int64?) case draftMessageEmpty(flags: Int32, date: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .draftMessage(let flags, let replyTo, let message, let entities, let media, let date): + case .draftMessage(let flags, let replyTo, let message, let entities, let media, let date, let effect): if boxed { - buffer.appendInt32(1070397423) + buffer.appendInt32(761606687) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 4) != 0 {replyTo!.serialize(buffer, true)} @@ -19,6 +19,7 @@ public extension Api { }} if Int(flags) & Int(1 << 5) != 0 {media!.serialize(buffer, true)} serializeInt32(date, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 7) != 0 {serializeInt64(effect!, buffer: buffer, boxed: false)} break case .draftMessageEmpty(let flags, let date): if boxed { @@ -32,8 +33,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .draftMessage(let flags, let replyTo, let message, let entities, let media, let date): - return ("draftMessage", [("flags", flags as Any), ("replyTo", replyTo as Any), ("message", message as Any), ("entities", entities as Any), ("media", media as Any), ("date", date as Any)]) + case .draftMessage(let flags, let replyTo, let message, let entities, let media, let date, let effect): + return ("draftMessage", [("flags", flags as Any), ("replyTo", replyTo as Any), ("message", message as Any), ("entities", entities as Any), ("media", media as Any), ("date", date as Any), ("effect", effect as Any)]) case .draftMessageEmpty(let flags, let date): return ("draftMessageEmpty", [("flags", flags as Any), ("date", date as Any)]) } @@ -58,14 +59,17 @@ public extension Api { } } var _6: Int32? _6 = reader.readInt32() + var _7: Int64? + if Int(_1!) & Int(1 << 7) != 0 {_7 = reader.readInt64() } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 4) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil let _c5 = (Int(_1!) & Int(1 << 5) == 0) || _5 != nil let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.DraftMessage.draftMessage(flags: _1!, replyTo: _2, message: _3!, entities: _4, media: _5, date: _6!) + let _c7 = (Int(_1!) & Int(1 << 7) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.DraftMessage.draftMessage(flags: _1!, replyTo: _2, message: _3!, entities: _4, media: _5, date: _6!, effect: _7) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api7.swift b/submodules/TelegramApi/Sources/Api7.swift index af25d1b0351..ac4e62f39d6 100644 --- a/submodules/TelegramApi/Sources/Api7.swift +++ b/submodules/TelegramApi/Sources/Api7.swift @@ -472,6 +472,50 @@ public extension Api { } } +public extension Api { + indirect enum FoundStory: TypeConstructorDescription { + case foundStory(peer: Api.Peer, story: Api.StoryItem) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .foundStory(let peer, let story): + if boxed { + buffer.appendInt32(-394605632) + } + peer.serialize(buffer, true) + story.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .foundStory(let peer, let story): + return ("foundStory", [("peer", peer as Any), ("story", story as Any)]) + } + } + + public static func parse_foundStory(_ reader: BufferReader) -> FoundStory? { + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _2: Api.StoryItem? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.StoryItem + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.FoundStory.foundStory(peer: _1!, story: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum Game: TypeConstructorDescription { case game(flags: Int32, id: Int64, accessHash: Int64, shortName: String, title: String, description: String, photo: Api.Photo, document: Api.Document?) @@ -604,6 +648,58 @@ public extension Api { } } +public extension Api { + enum GeoPointAddress: TypeConstructorDescription { + case geoPointAddress(flags: Int32, countryIso2: String, state: String?, city: String?, street: String?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .geoPointAddress(let flags, let countryIso2, let state, let city, let street): + if boxed { + buffer.appendInt32(-565420653) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(countryIso2, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(state!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeString(city!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeString(street!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .geoPointAddress(let flags, let countryIso2, let state, let city, let street): + return ("geoPointAddress", [("flags", flags as Any), ("countryIso2", countryIso2 as Any), ("state", state as Any), ("city", city as Any), ("street", street as Any)]) + } + } + + public static func parse_geoPointAddress(_ reader: BufferReader) -> GeoPointAddress? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } + var _4: String? + if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } + var _5: String? + if Int(_1!) & Int(1 << 2) != 0 {_5 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.GeoPointAddress.geoPointAddress(flags: _1!, countryIso2: _2!, state: _3, city: _4, street: _5) + } + else { + return nil + } + } + + } +} public extension Api { enum GlobalPrivacySettings: TypeConstructorDescription { case globalPrivacySettings(flags: Int32) @@ -1218,119 +1314,3 @@ public extension Api { } } -public extension Api { - enum InputAppEvent: TypeConstructorDescription { - case inputAppEvent(time: Double, type: String, peer: Int64, data: Api.JSONValue) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputAppEvent(let time, let type, let peer, let data): - if boxed { - buffer.appendInt32(488313413) - } - serializeDouble(time, buffer: buffer, boxed: false) - serializeString(type, buffer: buffer, boxed: false) - serializeInt64(peer, buffer: buffer, boxed: false) - data.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputAppEvent(let time, let type, let peer, let data): - return ("inputAppEvent", [("time", time as Any), ("type", type as Any), ("peer", peer as Any), ("data", data as Any)]) - } - } - - public static func parse_inputAppEvent(_ reader: BufferReader) -> InputAppEvent? { - var _1: Double? - _1 = reader.readDouble() - var _2: String? - _2 = parseString(reader) - var _3: Int64? - _3 = reader.readInt64() - var _4: Api.JSONValue? - if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.JSONValue - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.InputAppEvent.inputAppEvent(time: _1!, type: _2!, peer: _3!, data: _4!) - } - else { - return nil - } - } - - } -} -public extension Api { - indirect enum InputBotApp: TypeConstructorDescription { - case inputBotAppID(id: Int64, accessHash: Int64) - case inputBotAppShortName(botId: Api.InputUser, shortName: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputBotAppID(let id, let accessHash): - if boxed { - buffer.appendInt32(-1457472134) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputBotAppShortName(let botId, let shortName): - if boxed { - buffer.appendInt32(-1869872121) - } - botId.serialize(buffer, true) - serializeString(shortName, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputBotAppID(let id, let accessHash): - return ("inputBotAppID", [("id", id as Any), ("accessHash", accessHash as Any)]) - case .inputBotAppShortName(let botId, let shortName): - return ("inputBotAppShortName", [("botId", botId as Any), ("shortName", shortName as Any)]) - } - } - - public static func parse_inputBotAppID(_ reader: BufferReader) -> InputBotApp? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputBotApp.inputBotAppID(id: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputBotAppShortName(_ reader: BufferReader) -> InputBotApp? { - var _1: Api.InputUser? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputUser - } - var _2: String? - _2 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputBotApp.inputBotAppShortName(botId: _1!, shortName: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api8.swift b/submodules/TelegramApi/Sources/Api8.swift index 429d2a4e3a6..711c158ee00 100644 --- a/submodules/TelegramApi/Sources/Api8.swift +++ b/submodules/TelegramApi/Sources/Api8.swift @@ -1,3 +1,119 @@ +public extension Api { + enum InputAppEvent: TypeConstructorDescription { + case inputAppEvent(time: Double, type: String, peer: Int64, data: Api.JSONValue) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputAppEvent(let time, let type, let peer, let data): + if boxed { + buffer.appendInt32(488313413) + } + serializeDouble(time, buffer: buffer, boxed: false) + serializeString(type, buffer: buffer, boxed: false) + serializeInt64(peer, buffer: buffer, boxed: false) + data.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputAppEvent(let time, let type, let peer, let data): + return ("inputAppEvent", [("time", time as Any), ("type", type as Any), ("peer", peer as Any), ("data", data as Any)]) + } + } + + public static func parse_inputAppEvent(_ reader: BufferReader) -> InputAppEvent? { + var _1: Double? + _1 = reader.readDouble() + var _2: String? + _2 = parseString(reader) + var _3: Int64? + _3 = reader.readInt64() + var _4: Api.JSONValue? + if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.JSONValue + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputAppEvent.inputAppEvent(time: _1!, type: _2!, peer: _3!, data: _4!) + } + else { + return nil + } + } + + } +} +public extension Api { + indirect enum InputBotApp: TypeConstructorDescription { + case inputBotAppID(id: Int64, accessHash: Int64) + case inputBotAppShortName(botId: Api.InputUser, shortName: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputBotAppID(let id, let accessHash): + if boxed { + buffer.appendInt32(-1457472134) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputBotAppShortName(let botId, let shortName): + if boxed { + buffer.appendInt32(-1869872121) + } + botId.serialize(buffer, true) + serializeString(shortName, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputBotAppID(let id, let accessHash): + return ("inputBotAppID", [("id", id as Any), ("accessHash", accessHash as Any)]) + case .inputBotAppShortName(let botId, let shortName): + return ("inputBotAppShortName", [("botId", botId as Any), ("shortName", shortName as Any)]) + } + } + + public static func parse_inputBotAppID(_ reader: BufferReader) -> InputBotApp? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputBotApp.inputBotAppID(id: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputBotAppShortName(_ reader: BufferReader) -> InputBotApp? { + var _1: Api.InputUser? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputUser + } + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputBotApp.inputBotAppShortName(botId: _1!, shortName: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum InputBotInlineMessage: TypeConstructorDescription { case inputBotInlineMessageGame(flags: Int32, replyMarkup: Api.ReplyMarkup?) @@ -1196,99 +1312,3 @@ public extension Api { } } -public extension Api { - enum InputClientProxy: TypeConstructorDescription { - case inputClientProxy(address: String, port: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputClientProxy(let address, let port): - if boxed { - buffer.appendInt32(1968737087) - } - serializeString(address, buffer: buffer, boxed: false) - serializeInt32(port, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputClientProxy(let address, let port): - return ("inputClientProxy", [("address", address as Any), ("port", port as Any)]) - } - } - - public static func parse_inputClientProxy(_ reader: BufferReader) -> InputClientProxy? { - var _1: String? - _1 = parseString(reader) - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputClientProxy.inputClientProxy(address: _1!, port: _2!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum InputCollectible: TypeConstructorDescription { - case inputCollectiblePhone(phone: String) - case inputCollectibleUsername(username: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputCollectiblePhone(let phone): - if boxed { - buffer.appendInt32(-1562241884) - } - serializeString(phone, buffer: buffer, boxed: false) - break - case .inputCollectibleUsername(let username): - if boxed { - buffer.appendInt32(-476815191) - } - serializeString(username, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputCollectiblePhone(let phone): - return ("inputCollectiblePhone", [("phone", phone as Any)]) - case .inputCollectibleUsername(let username): - return ("inputCollectibleUsername", [("username", username as Any)]) - } - } - - public static func parse_inputCollectiblePhone(_ reader: BufferReader) -> InputCollectible? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.InputCollectible.inputCollectiblePhone(phone: _1!) - } - else { - return nil - } - } - public static func parse_inputCollectibleUsername(_ reader: BufferReader) -> InputCollectible? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.InputCollectible.inputCollectibleUsername(username: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api9.swift b/submodules/TelegramApi/Sources/Api9.swift index 3395b6c52d9..32c3e40b12e 100644 --- a/submodules/TelegramApi/Sources/Api9.swift +++ b/submodules/TelegramApi/Sources/Api9.swift @@ -1,3 +1,99 @@ +public extension Api { + enum InputClientProxy: TypeConstructorDescription { + case inputClientProxy(address: String, port: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputClientProxy(let address, let port): + if boxed { + buffer.appendInt32(1968737087) + } + serializeString(address, buffer: buffer, boxed: false) + serializeInt32(port, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputClientProxy(let address, let port): + return ("inputClientProxy", [("address", address as Any), ("port", port as Any)]) + } + } + + public static func parse_inputClientProxy(_ reader: BufferReader) -> InputClientProxy? { + var _1: String? + _1 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputClientProxy.inputClientProxy(address: _1!, port: _2!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum InputCollectible: TypeConstructorDescription { + case inputCollectiblePhone(phone: String) + case inputCollectibleUsername(username: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputCollectiblePhone(let phone): + if boxed { + buffer.appendInt32(-1562241884) + } + serializeString(phone, buffer: buffer, boxed: false) + break + case .inputCollectibleUsername(let username): + if boxed { + buffer.appendInt32(-476815191) + } + serializeString(username, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputCollectiblePhone(let phone): + return ("inputCollectiblePhone", [("phone", phone as Any)]) + case .inputCollectibleUsername(let username): + return ("inputCollectibleUsername", [("username", username as Any)]) + } + } + + public static func parse_inputCollectiblePhone(_ reader: BufferReader) -> InputCollectible? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputCollectible.inputCollectiblePhone(phone: _1!) + } + else { + return nil + } + } + public static func parse_inputCollectibleUsername(_ reader: BufferReader) -> InputCollectible? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputCollectible.inputCollectibleUsername(username: _1!) + } + else { + return nil + } + } + + } +} public extension Api { enum InputContact: TypeConstructorDescription { case inputPhoneContact(clientId: Int64, phone: String, firstName: String, lastName: String) diff --git a/submodules/TelegramCallsUI/BUILD b/submodules/TelegramCallsUI/BUILD index b73b781fa96..2c3825434ee 100644 --- a/submodules/TelegramCallsUI/BUILD +++ b/submodules/TelegramCallsUI/BUILD @@ -110,6 +110,7 @@ swift_library( "//submodules/TinyThumbnail", "//submodules/ImageBlur", "//submodules/MetalEngine", + "//submodules/TelegramUI/Components/Calls/VoiceChatActionButton", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index 2eff0f14555..934725f5431 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -716,7 +716,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP interfaceOrientation: layout.metrics.orientation ?? .portrait, screenCornerRadius: layout.deviceMetrics.screenCornerRadius, state: callScreenState, - transition: Transition(transition) + transition: ComponentTransition(transition) ) } } @@ -784,7 +784,7 @@ private func copyI420BufferToNV12Buffer(buffer: OngoingGroupCallContext.VideoFra return true } -private final class AdaptedCallVideoSource: VideoSource { +final class AdaptedCallVideoSource: VideoSource { final class I420DataBuffer: Output.DataBuffer { private let buffer: OngoingGroupCallContext.VideoFrameData.I420Buffer diff --git a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift index 19b7211e2d5..e6b0fbf432f 100644 --- a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift @@ -9,6 +9,7 @@ import TelegramPresentationData import TelegramUIPreferences import AccountContext import AnimatedCountLabelNode +import VoiceChatActionButton private let blue = UIColor(rgb: 0x007fff) private let lightBlue = UIColor(rgb: 0x00affe) diff --git a/submodules/TelegramCallsUI/Sources/Components/AnimatedCounterView.swift b/submodules/TelegramCallsUI/Sources/Components/AnimatedCounterView.swift index 8ebe446403f..3327eb027ec 100644 --- a/submodules/TelegramCallsUI/Sources/Components/AnimatedCounterView.swift +++ b/submodules/TelegramCallsUI/Sources/Components/AnimatedCounterView.swift @@ -43,7 +43,7 @@ public final class AnimatedCountView: UIView { self.updateFrames() } - func updateFrames(transition: ComponentFlow.Transition? = nil) { + func updateFrames(transition: ComponentFlow.ComponentTransition? = nil) { let subtitleHeight: CGFloat = subtitleLabel.intrinsicContentSize.height let subtitleFrame = CGRect(x: bounds.midX - subtitleLabel.intrinsicContentSize.width / 2 - 10, y: self.countLabel.attributedText?.length == 0 ? bounds.midY - subtitleHeight / 2 : bounds.height - subtitleHeight, width: subtitleLabel.intrinsicContentSize.width + 20, height: subtitleHeight) if let transition { diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index a70300baeeb..35877a0bd4f 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -196,7 +196,7 @@ public final class MediaStreamComponent: CombinedComponent { func toggleDisplayUI() { self.displayUI = !self.displayUI - self.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .easeInOut))) + self.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .easeInOut))) } func cancelScheduledDismissUI() { @@ -224,7 +224,7 @@ public final class MediaStreamComponent: CombinedComponent { if interactive { self.updated(transition: .immediate) } else { - self.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + self.updated(transition: ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))) } } } @@ -1552,7 +1552,7 @@ private final class StreamTitleComponent: Component { } } - func update(component: StreamTitleComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: StreamTitleComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let liveIndicatorWidth: CGFloat = self.liveIndicatorView.desiredWidth let liveIndicatorHeight: CGFloat = 20.0 @@ -1686,7 +1686,7 @@ private final class StreamTitleComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -2024,7 +2024,7 @@ final class RoundGradientButtonComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: RoundGradientButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: RoundGradientButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.iconView.image = component.image ?? component.icon.flatMap { UIImage(bundleImageName: $0) } let gradientColors: [CGColor] if component.gradientColors.count == 1 { @@ -2062,7 +2062,7 @@ final class RoundGradientButtonComponent: Component { View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift index 4bda7c0fca5..2debd4072b7 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift @@ -193,7 +193,7 @@ final class MediaStreamVideoComponent: Component { } } - private func updateVideoStalled(isStalled: Bool, transition: Transition?) { + private func updateVideoStalled(isStalled: Bool, transition: ComponentTransition?) { if isStalled { guard let component = self.component else { return } @@ -282,7 +282,7 @@ final class MediaStreamVideoComponent: Component { } } - func update(component: MediaStreamVideoComponent, availableSize: CGSize, state: State, transition: Transition) -> CGSize { + func update(component: MediaStreamVideoComponent, availableSize: CGSize, state: State, transition: ComponentTransition) -> CGSize { self.state = state self.component = component self.onVideoPlaybackChange = component.onVideoPlaybackLiveChange @@ -332,24 +332,8 @@ final class MediaStreamVideoComponent: Component { }) stallTimer = _stallTimer self.clipsToBounds = component.isFullscreen // or just true - if let videoBlurView = self.videoRenderingContext.makeView(input: input, blur: true) { - self.videoBlurView = videoBlurView - self.insertSubview(videoBlurView, belowSubview: self.blurTintView) - videoBlurView.alpha = 0 - UIView.animate(withDuration: 0.3) { - videoBlurView.alpha = 1 - } - self.videoBlurGradientMask.type = .radial - self.videoBlurGradientMask.colors = [UIColor(rgb: 0x000000, alpha: 0.5).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor] - self.videoBlurGradientMask.startPoint = CGPoint(x: 0.5, y: 0.5) - self.videoBlurGradientMask.endPoint = CGPoint(x: 1.0, y: 1.0) - - self.videoBlurSolidMask.backgroundColor = UIColor.black.cgColor - self.videoBlurGradientMask.addSublayer(videoBlurSolidMask) - - } - - if let videoView = self.videoRenderingContext.makeView(input: input, blur: false, forceSampleBufferDisplayLayer: true) { + + if let videoView = self.videoRenderingContext.makeView(input: input, forceSampleBufferDisplayLayer: true) { self.videoView = videoView self.addSubview(videoView) videoView.alpha = 0 @@ -432,6 +416,23 @@ final class MediaStreamVideoComponent: Component { state?.updated(transition: .immediate) } } + + if let videoView = self.videoView, let videoBlurView = self.videoRenderingContext.makeBlurView(input: input, mainView: videoView) { + self.videoBlurView = videoBlurView + self.insertSubview(videoBlurView, belowSubview: self.blurTintView) + videoBlurView.alpha = 0 + UIView.animate(withDuration: 0.3) { + videoBlurView.alpha = 1 + } + self.videoBlurGradientMask.type = .radial + self.videoBlurGradientMask.colors = [UIColor(rgb: 0x000000, alpha: 0.5).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor] + self.videoBlurGradientMask.startPoint = CGPoint(x: 0.5, y: 0.5) + self.videoBlurGradientMask.endPoint = CGPoint(x: 1.0, y: 1.0) + + self.videoBlurSolidMask.backgroundColor = UIColor.black.cgColor + self.videoBlurGradientMask.addSublayer(videoBlurSolidMask) + + } } } else if component.isFullscreen { if fullScreenBackgroundPlaceholder.superview == nil { @@ -458,7 +459,7 @@ final class MediaStreamVideoComponent: Component { let videoSize: CGSize let videoCornerRadius: CGFloat = component.isFullscreen ? 0 : 10 - let videoFrameUpdateTransition: Transition + let videoFrameUpdateTransition: ComponentTransition if self.wasFullscreen != component.isFullscreen { videoFrameUpdateTransition = transition } else { @@ -550,7 +551,7 @@ final class MediaStreamVideoComponent: Component { if loadingBlurView.frame == .zero { loadingBlurView.frame = loadingBlurViewFrame } else { - // Using Transition.setFrame on UIVisualEffectView causes instant update of sublayers + // Using ComponentTransition.setFrame on UIVisualEffectView causes instant update of sublayers switch videoFrameUpdateTransition.animation { case let .curve(duration, curve): UIView.animate(withDuration: duration, delay: 0, options: curve.containedViewLayoutTransitionCurve.viewAnimationOptions, animations: { [self] in @@ -740,7 +741,7 @@ final class MediaStreamVideoComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, transition: transition) } } diff --git a/submodules/TelegramCallsUI/Sources/Components/ParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/Components/ParticipantsComponent.swift index 29e1b9d25e1..185dda5c488 100644 --- a/submodules/TelegramCallsUI/Sources/Components/ParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/ParticipantsComponent.swift @@ -43,7 +43,7 @@ final class ParticipantsComponent: Component { View(frame: .zero) } - func update(view: View, availableSize: CGSize, state: ComponentFlow.EmptyComponentState, environment: ComponentFlow.Environment, transition: ComponentFlow.Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: ComponentFlow.EmptyComponentState, environment: ComponentFlow.Environment, transition: ComponentFlow.ComponentTransition) -> CGSize { view.counter.update( countString: self.count > 0 ? presentationStringsFormattedNumber(Int32(count), ",") : "", subtitle: self.showsSubtitle ? (self.count > 0 ? self.strings.LiveStream_Watching.lowercased() : self.strings.LiveStream_NoViewers.lowercased()) : "", diff --git a/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift b/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift index 54b037d457b..9f568732b9e 100644 --- a/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift @@ -106,7 +106,7 @@ final class StreamSheetComponent: CombinedComponent { return false } - func update(component: StreamSheetComponent, availableSize: CGSize, state: State, transition: Transition) -> CGSize { + func update(component: StreamSheetComponent, availableSize: CGSize, state: State, transition: ComponentTransition) -> CGSize { return availableSize } @@ -202,7 +202,7 @@ final class SheetBackgroundComponent: Component { class View: UIView { private let backgroundView = UIView() - func update(availableSize: CGSize, color: UIColor, cornerRadius: CGFloat, offset: CGFloat, transition: Transition) { + func update(availableSize: CGSize, color: UIColor, cornerRadius: CGFloat, offset: CGFloat, transition: ComponentTransition) { if backgroundView.superview == nil { self.addSubview(backgroundView) } @@ -262,7 +262,7 @@ final class SheetBackgroundComponent: Component { self.offset = offset } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { view.update(availableSize: availableSize, color: color, cornerRadius: radius, offset: offset, transition: transition) return availableSize } diff --git a/submodules/TelegramCallsUI/Sources/GroupVideoNode.swift b/submodules/TelegramCallsUI/Sources/GroupVideoNode.swift index 282a384d67e..bf737532ff5 100644 --- a/submodules/TelegramCallsUI/Sources/GroupVideoNode.swift +++ b/submodules/TelegramCallsUI/Sources/GroupVideoNode.swift @@ -305,6 +305,8 @@ final class GroupVideoNode: ASDisplayNode, PreviewVideoNode { transition.updatePosition(layer: self.videoView.layer, position: rotatedVideoFrame.center) transition.updateBounds(layer: self.videoView.layer, bounds: CGRect(origin: CGPoint(), size: normalizedVideoSize)) + self.videoView.updateLayout(size: normalizedVideoSize, transition: transition) + let transformScale: CGFloat = rotatedVideoFrame.width / normalizedVideoSize.width transition.updateTransformScale(layer: self.videoViewContainer.layer, scale: transformScale) @@ -340,6 +342,7 @@ final class GroupVideoNode: ASDisplayNode, PreviewVideoNode { }) transition.updateBounds(layer: backdropVideoView.layer, bounds: CGRect(origin: CGPoint(), size: normalizedVideoSize)) + backdropVideoView.updateLayout(size: normalizedVideoSize, transition: transition) let transformScale: CGFloat = rotatedVideoFrame.width / normalizedVideoSize.width diff --git a/submodules/TelegramCallsUI/Sources/MetalVideoRenderingView.swift b/submodules/TelegramCallsUI/Sources/MetalVideoRenderingView.swift index 13f4974b7a1..b2ae7332247 100644 --- a/submodules/TelegramCallsUI/Sources/MetalVideoRenderingView.swift +++ b/submodules/TelegramCallsUI/Sources/MetalVideoRenderingView.swift @@ -514,6 +514,9 @@ final class MetalVideoRenderingView: UIView, VideoRenderingView { } } } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + } } @available(iOS 13.0, *) diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index ed0b1e120ee..284f939f915 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -853,6 +853,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { public let isStream: Bool + public var onMutedSpeechActivityDetected: ((Bool) -> Void)? + init( accountContext: AccountContext, audioSession: ManagedAudioSession, @@ -1635,7 +1637,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { if let current = self.genericCallContext { genericCallContext = current } else { - if self.isStream, !"".isEmpty { + if self.isStream, self.accountContext.sharedContext.immediateExperimentalUISettings.liveStreamV2 { genericCallContext = .mediaStream(WrappedMediaStreamingContext(rejoinNeeded: { [weak self] in Queue.mainQueue().async { guard let strongSelf = self else { @@ -1674,8 +1676,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { strongSelf.requestCall(movingFromBroadcastToRtc: false) } } - }, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: self.isVideoEnabled ? .generic : .none, enableNoiseSuppression: false, disableAudioInput: self.isStream, preferX264: self.accountContext.sharedContext.immediateExperimentalUISettings.preferredVideoCodec == "H264", logPath: allocateCallLogPath(account: self.account) - )) + }, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: self.isVideoEnabled ? .generic : .none, enableNoiseSuppression: false, disableAudioInput: self.isStream, preferX264: self.accountContext.sharedContext.immediateExperimentalUISettings.preferredVideoCodec == "H264", logPath: allocateCallLogPath(account: self.account), onMutedSpeechActivityDetected: { [weak self] value in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + strongSelf.onMutedSpeechActivityDetected?(value) + } + })) } self.genericCallContext = genericCallContext @@ -2967,7 +2975,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.hasScreencast = true - let screencastCallContext = OngoingGroupCallContext(audioSessionActive: .single(true), video: self.screencastCapturer, requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, rejoinNeeded: { }, outgoingAudioBitrateKbit: nil, videoContentType: .screencast, enableNoiseSuppression: false, disableAudioInput: true, preferX264: false, logPath: "") + let screencastCallContext = OngoingGroupCallContext(audioSessionActive: .single(true), video: self.screencastCapturer, requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, rejoinNeeded: { }, outgoingAudioBitrateKbit: nil, videoContentType: .screencast, enableNoiseSuppression: false, disableAudioInput: true, preferX264: false, logPath: "", onMutedSpeechActivityDetected: { _ in }) self.screencastCallContext = screencastCallContext self.screencastJoinDisposable.set((screencastCallContext.joinPayload diff --git a/submodules/TelegramCallsUI/Sources/SampleBufferVideoRenderingView.swift b/submodules/TelegramCallsUI/Sources/SampleBufferVideoRenderingView.swift index 9e45116cf0c..6ac153e02e9 100644 --- a/submodules/TelegramCallsUI/Sources/SampleBufferVideoRenderingView.swift +++ b/submodules/TelegramCallsUI/Sources/SampleBufferVideoRenderingView.swift @@ -226,4 +226,7 @@ final class SampleBufferVideoRenderingView: UIView, VideoRenderingView { func updateIsEnabled(_ isEnabled: Bool) { self.isEnabled = isEnabled } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + } } diff --git a/submodules/TelegramCallsUI/Sources/VideoRenderingContext.swift b/submodules/TelegramCallsUI/Sources/VideoRenderingContext.swift index 9c043ac4549..59256f13e77 100644 --- a/submodules/TelegramCallsUI/Sources/VideoRenderingContext.swift +++ b/submodules/TelegramCallsUI/Sources/VideoRenderingContext.swift @@ -6,6 +6,8 @@ import SwiftSignalKit import AccountContext import TelegramVoip import AVFoundation +import CallScreen +import MetalEngine protocol VideoRenderingView: UIView { func setOnFirstFrameReceived(_ f: @escaping (Float) -> Void) @@ -14,6 +16,7 @@ protocol VideoRenderingView: UIView { func getAspect() -> CGFloat func setOnIsMirroredUpdated(_ f: @escaping (Bool) -> Void) func updateIsEnabled(_ isEnabled: Bool) + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) } class VideoRenderingContext { @@ -33,24 +36,38 @@ class VideoRenderingContext { } #endif - func makeView(input: Signal, blur: Bool, forceSampleBufferDisplayLayer: Bool = false) -> VideoRenderingView? { + func makeView(input: Signal, forceSampleBufferDisplayLayer: Bool = false) -> VideoRenderingView? { + if !forceSampleBufferDisplayLayer { + return CallScreenVideoView(input: input) + } + #if targetEnvironment(simulator) - if blur { - #if DEBUG + return SampleBufferVideoRenderingView(input: input) + #else + if #available(iOS 13.0, *), !forceSampleBufferDisplayLayer { + return MetalVideoRenderingView(renderingContext: self.metalContext, input: input, blur: false) + } else { return SampleBufferVideoRenderingView(input: input) - #else - return nil - #endif } + #endif + } + + func makeBlurView(input: Signal, mainView: VideoRenderingView?, forceSampleBufferDisplayLayer: Bool = false) -> VideoRenderingView? { + if let mainView = mainView as? CallScreenVideoView { + return CallScreenVideoBlurView(mainView: mainView) + } + + #if targetEnvironment(simulator) + #if DEBUG return SampleBufferVideoRenderingView(input: input) #else + return nil + #endif + #else if #available(iOS 13.0, *), !forceSampleBufferDisplayLayer { - return MetalVideoRenderingView(renderingContext: self.metalContext, input: input, blur: blur) + return MetalVideoRenderingView(renderingContext: self.metalContext, input: input, blur: true) } else { - if blur { - return nil - } - return SampleBufferVideoRenderingView(input: input) + return nil } #endif } @@ -79,3 +96,189 @@ extension PresentationCallVideoView.Orientation { } } } + +private final class CallScreenVideoView: UIView, VideoRenderingView { + private var isEnabled: Bool = false + + private var onFirstFrameReceived: ((Float) -> Void)? + private var onOrientationUpdated: ((PresentationCallVideoView.Orientation, CGFloat) -> Void)? + private var onIsMirroredUpdated: ((Bool) -> Void)? + + private var didReportFirstFrame: Bool = false + private var currentIsMirrored: Bool = false + private var currentOrientation: PresentationCallVideoView.Orientation = .rotation0 + private var currentAspect: CGFloat = 1.0 + + fileprivate let videoSource: AdaptedCallVideoSource + private var disposable: Disposable? + + fileprivate let videoLayer: PrivateCallVideoLayer + + init(input: Signal) { + self.videoLayer = PrivateCallVideoLayer() + self.videoLayer.masksToBounds = true + + self.videoSource = AdaptedCallVideoSource(videoStreamSignal: input) + + super.init(frame: CGRect()) + + self.layer.addSublayer(self.videoLayer) + + self.disposable = self.videoSource.addOnUpdated { [weak self] in + guard let self else { + return + } + + self.videoLayer.video = self.videoSource.currentOutput + + var notifyOrientationUpdated = false + var notifyIsMirroredUpdated = false + + if !self.didReportFirstFrame { + notifyOrientationUpdated = true + notifyIsMirroredUpdated = true + } + + if let currentOutput = self.videoSource.currentOutput { + let currentAspect: CGFloat + if currentOutput.resolution.height > 0.0 { + currentAspect = currentOutput.resolution.width / currentOutput.resolution.height + } else { + currentAspect = 1.0 + } + if self.currentAspect != currentAspect { + self.currentAspect = currentAspect + notifyOrientationUpdated = true + } + + let currentOrientation: PresentationCallVideoView.Orientation + if abs(currentOutput.rotationAngle - 0.0) < .ulpOfOne { + currentOrientation = .rotation0 + } else if abs(currentOutput.rotationAngle - Float.pi * 0.5) < .ulpOfOne { + currentOrientation = .rotation90 + } else if abs(currentOutput.rotationAngle - Float.pi) < .ulpOfOne { + currentOrientation = .rotation180 + } else if abs(currentOutput.rotationAngle - Float.pi * 3.0 / 2.0) < .ulpOfOne { + currentOrientation = .rotation270 + } else { + currentOrientation = .rotation0 + } + if self.currentOrientation != currentOrientation { + self.currentOrientation = currentOrientation + notifyOrientationUpdated = true + } + + let currentIsMirrored = !currentOutput.mirrorDirection.isEmpty + if self.currentIsMirrored != currentIsMirrored { + self.currentIsMirrored = currentIsMirrored + notifyIsMirroredUpdated = true + } + } + + if !self.didReportFirstFrame { + self.didReportFirstFrame = true + self.onFirstFrameReceived?(Float(self.currentAspect)) + } + + if notifyOrientationUpdated { + self.onOrientationUpdated?(self.currentOrientation, self.currentAspect) + } + + if notifyIsMirroredUpdated { + self.onIsMirroredUpdated?(self.currentIsMirrored) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposable?.dispose() + } + + func setOnFirstFrameReceived(_ f: @escaping (Float) -> Void) { + self.onFirstFrameReceived = f + self.didReportFirstFrame = false + } + + func setOnOrientationUpdated(_ f: @escaping (PresentationCallVideoView.Orientation, CGFloat) -> Void) { + self.onOrientationUpdated = f + } + + func getOrientation() -> PresentationCallVideoView.Orientation { + return self.currentOrientation + } + + func getAspect() -> CGFloat { + return self.currentAspect + } + + func setOnIsMirroredUpdated(_ f: @escaping (Bool) -> Void) { + self.onIsMirroredUpdated = f + } + + func updateIsEnabled(_ isEnabled: Bool) { + self.isEnabled = isEnabled + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + if let currentOutput = self.videoSource.currentOutput { + let rotatedResolution = currentOutput.resolution + let videoSize = size + + let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: 1280, height: 1280)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0)) + let rotatedVideoResolution = videoResolution + + transition.updateFrame(layer: self.videoLayer, frame: CGRect(origin: CGPoint(), size: size)) + self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2) + } + } +} + +private final class CallScreenVideoBlurView: UIView, VideoRenderingView { + private weak var mainView: CallScreenVideoView? + + private let blurredLayer: MetalEngineSubjectLayer + + init(mainView: CallScreenVideoView) { + self.mainView = mainView + self.blurredLayer = mainView.videoLayer.blurredLayer + + super.init(frame: CGRect()) + + self.layer.addSublayer(self.blurredLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + func setOnFirstFrameReceived(_ f: @escaping (Float) -> Void) { + } + + func setOnOrientationUpdated(_ f: @escaping (PresentationCallVideoView.Orientation, CGFloat) -> Void) { + } + + func getOrientation() -> PresentationCallVideoView.Orientation { + return .rotation0 + } + + func getAspect() -> CGFloat { + return 1.0 + } + + func setOnIsMirroredUpdated(_ f: @escaping (Bool) -> Void) { + } + + func updateIsEnabled(_ isEnabled: Bool) { + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(layer: self.blurredLayer, frame: CGRect(origin: CGPoint(), size: size)) + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift b/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift deleted file mode 100644 index 0ede7e52093..00000000000 --- a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift +++ /dev/null @@ -1,1760 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import SwiftSignalKit -import AnimationUI -import AppBundle -import ManagedAnimationNode - -private let titleFont = Font.regular(15.0) -private let subtitleFont = Font.regular(13.0) - -private let white = UIColor(rgb: 0xffffff) -private let greyColor = UIColor(rgb: 0x2c2c2e) -private let secondaryGreyColor = UIColor(rgb: 0x1c1c1e) -private let blue = UIColor(rgb: 0x007fff) -private let lightBlue = UIColor(rgb: 0x00affe) -private let green = UIColor(rgb: 0x33c659) -private let activeBlue = UIColor(rgb: 0x00a0b9) -private let purple = UIColor(rgb: 0x3252ef) -private let pink = UIColor(rgb: 0xef436c) - -private let areaSize = CGSize(width: 300.0, height: 300.0) -private let blobSize = CGSize(width: 190.0, height: 190.0) - -private let smallScale: CGFloat = 0.48 -private let smallIconScale: CGFloat = 0.69 - -private let buttonHeight: CGFloat = 52.0 - -final class VoiceChatActionButton: HighlightTrackingButtonNode { - enum State: Equatable { - enum ActiveState: Equatable { - case cantSpeak - case muted - case on - } - - enum ScheduledState: Equatable { - case start - case subscribe - case unsubscribe - } - - case button(text: String) - case scheduled(state: ScheduledState) - case connecting - case active(state: ActiveState) - } - - var stateValue: State { - return self.currentParams?.state ?? .connecting - } - var statePromise = ValuePromise() - var state: Signal { - return self.statePromise.get() - } - - let bottomNode: ASDisplayNode - private let containerNode: ASDisplayNode - private let backgroundNode: VoiceChatActionButtonBackgroundNode - private let iconNode: VoiceChatActionButtonIconNode - private let labelContainerNode: ASDisplayNode - let titleLabel: ImmediateTextNode - private let subtitleLabel: ImmediateTextNode - private let buttonTitleLabel: ImmediateTextNode - - private var currentParams: (size: CGSize, buttonSize: CGSize, state: VoiceChatActionButton.State, dark: Bool, small: Bool, title: String, subtitle: String, snap: Bool)? - - private var activePromise = ValuePromise(false) - private var outerColorPromise = Promise<(UIColor?, UIColor?)>((nil, nil)) - var outerColor: Signal<(UIColor?, UIColor?), NoError> { - return self.outerColorPromise.get() - } - - var connectingColor: UIColor = UIColor(rgb: 0xb6b6bb) { - didSet { - self.backgroundNode.connectingColor = self.connectingColor - } - } - - var activeDisposable = MetaDisposable() - - var isDisabled: Bool = false - - var ignoreHierarchyChanges: Bool { - get { - return self.backgroundNode.ignoreHierarchyChanges - } set { - self.backgroundNode.ignoreHierarchyChanges = newValue - } - } - - var wasActiveWhenPressed = false - var pressing: Bool = false { - didSet { - guard let (_, _, state, _, small, _, _, snap) = self.currentParams, !self.isDisabled else { - return - } - if self.pressing { - let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) - if small { - transition.updateTransformScale(node: self.backgroundNode, scale: smallScale * 0.9) - transition.updateTransformScale(node: self.iconNode, scale: smallIconScale * 0.9) - } else { - transition.updateTransformScale(node: self.iconNode, scale: snap ? 0.5 : 0.9) - } - - switch state { - case let .active(state): - switch state { - case .on: - self.wasActiveWhenPressed = true - default: - break - } - case .connecting, .button, .scheduled: - break - } - } else { - let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) - if small { - transition.updateTransformScale(node: self.backgroundNode, scale: smallScale) - transition.updateTransformScale(node: self.iconNode, scale: smallIconScale) - } else { - transition.updateTransformScale(node: self.iconNode, scale: snap ? 0.5 : 1.0) - } - self.wasActiveWhenPressed = false - } - } - } - - var animationsEnabled: Bool = true { - didSet { - self.backgroundNode.animationsEnabled = self.animationsEnabled - } - } - - init() { - self.bottomNode = ASDisplayNode() - self.bottomNode.isUserInteractionEnabled = false - self.containerNode = ASDisplayNode() - self.containerNode.isUserInteractionEnabled = false - self.backgroundNode = VoiceChatActionButtonBackgroundNode() - self.iconNode = VoiceChatActionButtonIconNode(isColored: false) - - self.labelContainerNode = ASDisplayNode() - self.titleLabel = ImmediateTextNode() - self.subtitleLabel = ImmediateTextNode() - self.buttonTitleLabel = ImmediateTextNode() - self.buttonTitleLabel.isUserInteractionEnabled = false - self.buttonTitleLabel.alpha = 0.0 - - super.init() - - self.addSubnode(self.bottomNode) - self.labelContainerNode.addSubnode(self.titleLabel) - self.labelContainerNode.addSubnode(self.subtitleLabel) - self.addSubnode(self.labelContainerNode) - - self.addSubnode(self.containerNode) - self.containerNode.addSubnode(self.backgroundNode) - self.containerNode.addSubnode(self.iconNode) - - self.containerNode.addSubnode(self.buttonTitleLabel) - - self.highligthedChanged = { [weak self] pressing in - if let strongSelf = self { - guard let (_, _, state, _, small, _, _, snap) = strongSelf.currentParams else { - return - } - if pressing { - if case .button = state { - strongSelf.containerNode.layer.removeAnimation(forKey: "opacity") - strongSelf.containerNode.alpha = 0.4 - } else { - let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) - if small { - transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale * 0.9) - transition.updateTransformScale(node: strongSelf.iconNode, scale: smallIconScale * 0.9) - } else { - transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 0.9) - } - } - } else if !strongSelf.pressing { - if case .button = state { - strongSelf.containerNode.alpha = 1.0 - strongSelf.containerNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } else { - let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) - if small { - transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale) - transition.updateTransformScale(node: strongSelf.iconNode, scale: smallIconScale) - } else { - transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 1.0) - } - } - } - } - } - - self.backgroundNode.updatedActive = { [weak self] active in - self?.activePromise.set(active) - } - - self.backgroundNode.updatedColors = { [weak self] outerColor, activeColor in - self?.outerColorPromise.set(.single((outerColor, activeColor))) - } - } - - deinit { - self.activeDisposable.dispose() - } - - func updateLevel(_ level: CGFloat, immediately: Bool = false) { - self.backgroundNode.audioLevel = level - } - - private func applyParams(animated: Bool) { - guard let (size, _, state, _, small, title, subtitle, snap) = self.currentParams else { - return - } - - let updatedTitle = self.titleLabel.attributedText?.string != title - let updatedSubtitle = self.subtitleLabel.attributedText?.string != subtitle - - self.titleLabel.attributedText = NSAttributedString(string: title, font: titleFont, textColor: .white) - self.subtitleLabel.attributedText = NSAttributedString(string: subtitle, font: subtitleFont, textColor: .white) - - if animated && self.titleLabel.alpha > 0.0 { - if let snapshotView = self.titleLabel.view.snapshotContentTree(), updatedTitle { - self.titleLabel.view.superview?.insertSubview(snapshotView, belowSubview: self.titleLabel.view) - snapshotView.frame = self.titleLabel.frame - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - self.titleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - } - if let snapshotView = self.subtitleLabel.view.snapshotContentTree(), updatedSubtitle { - self.subtitleLabel.view.superview?.insertSubview(snapshotView, belowSubview: self.subtitleLabel.view) - snapshotView.frame = self.subtitleLabel.frame - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - self.subtitleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - } - } - - let titleSize = self.titleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude)) - let subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude)) - let totalHeight = titleSize.height + subtitleSize.height + 1.0 - - self.labelContainerNode.frame = CGRect(origin: CGPoint(), size: size) - - let titleLabelFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - totalHeight) / 2.0) + 84.0), size: titleSize) - let subtitleLabelFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: titleLabelFrame.maxY + 1.0), size: subtitleSize) - - self.titleLabel.bounds = CGRect(origin: CGPoint(), size: titleLabelFrame.size) - self.titleLabel.position = titleLabelFrame.center - self.subtitleLabel.bounds = CGRect(origin: CGPoint(), size: subtitleLabelFrame.size) - self.subtitleLabel.position = subtitleLabelFrame.center - - self.bottomNode.frame = CGRect(origin: CGPoint(), size: size) - self.containerNode.frame = CGRect(origin: CGPoint(), size: size) - - self.backgroundNode.bounds = CGRect(origin: CGPoint(), size: size) - self.backgroundNode.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0) - - var active = false - switch state { - case let .active(state): - switch state { - case .on: - active = self.pressing && !self.wasActiveWhenPressed - default: - break - } - case .connecting, .button, .scheduled: - break - } - - if snap { - let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate - transition.updateTransformScale(node: self.backgroundNode, scale: active ? 0.9 : 0.625) - transition.updateTransformScale(node: self.iconNode, scale: 0.625) - transition.updateAlpha(node: self.titleLabel, alpha: 0.0) - transition.updateAlpha(node: self.subtitleLabel, alpha: 0.0) - transition.updateAlpha(layer: self.backgroundNode.maskProgressLayer, alpha: 0.0) - } else { - let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate - if small { - transition.updateTransformScale(node: self.backgroundNode, scale: self.pressing ? smallScale * 0.9 : smallScale, delay: 0.0) - transition.updateTransformScale(node: self.iconNode, scale: self.pressing ? smallIconScale * 0.9 : smallIconScale, delay: 0.0) - transition.updateAlpha(node: self.titleLabel, alpha: 0.0) - transition.updateAlpha(node: self.subtitleLabel, alpha: 0.0) - transition.updateSublayerTransformOffset(layer: self.labelContainerNode.layer, offset: CGPoint(x: 0.0, y: -43.0)) - transition.updateTransformScale(node: self.titleLabel, scale: 0.8) - transition.updateTransformScale(node: self.subtitleLabel, scale: 0.8) - } else { - transition.updateTransformScale(node: self.backgroundNode, scale: 1.0, delay: 0.0) - transition.updateTransformScale(node: self.iconNode, scale: self.pressing ? 0.9 : 1.0, delay: 0.0) - transition.updateAlpha(node: self.titleLabel, alpha: 1.0, delay: 0.05) - transition.updateAlpha(node: self.subtitleLabel, alpha: 1.0, delay: 0.05) - transition.updateSublayerTransformOffset(layer: self.labelContainerNode.layer, offset: CGPoint()) - transition.updateTransformScale(node: self.titleLabel, scale: 1.0) - transition.updateTransformScale(node: self.subtitleLabel, scale: 1.0) - } - transition.updateAlpha(layer: self.backgroundNode.maskProgressLayer, alpha: 1.0) - } - - let iconSize = CGSize(width: 100.0, height: 100.0) - self.iconNode.bounds = CGRect(origin: CGPoint(), size: iconSize) - self.iconNode.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0) - } - - private var previousIcon: VoiceChatActionButtonIconAnimationState? - private func applyIconParams() { - guard let (_, _, state, _, _, _, _, _) = self.currentParams else { - return - } - - let icon: VoiceChatActionButtonIconAnimationState - switch state { - case .button: - icon = .empty - case let .scheduled(state): - switch state { - case .start: - icon = .start - case .subscribe: - icon = .subscribe - case .unsubscribe: - icon = .unsubscribe - } - case let .active(state): - switch state { - case .on: - icon = .unmute - case .muted: - icon = .mute - case .cantSpeak: - icon = .hand - } - case .connecting: - if let previousIcon = previousIcon { - icon = previousIcon - } else { - icon = .mute - } - } - self.previousIcon = icon - - self.iconNode.enqueueState(icon) - } - - func update(snap: Bool, animated: Bool) { - if let previous = self.currentParams { - self.currentParams = (previous.size, previous.buttonSize, previous.state, previous.dark, previous.small, previous.title, previous.subtitle, snap) - - self.backgroundNode.isSnap = snap - self.backgroundNode.glowHidden = snap || previous.small - self.backgroundNode.updateColors() - self.applyParams(animated: animated) - self.applyIconParams() - } - } - - func update(size: CGSize, buttonSize: CGSize, state: VoiceChatActionButton.State, title: String, subtitle: String, dark: Bool, small: Bool, animated: Bool = false) { - let previous = self.currentParams - let previousState = previous?.state - self.currentParams = (size, buttonSize, state, dark, small, title, subtitle, previous?.snap ?? false) - - self.statePromise.set(state) - - if let previousState = previousState, case .button = previousState, case .scheduled = state { - self.buttonTitleLabel.alpha = 0.0 - self.buttonTitleLabel.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) - self.buttonTitleLabel.layer.animateScale(from: 1.0, to: 0.001, duration: 0.24) - - self.iconNode.alpha = 1.0 - self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.iconNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0) - } - - var backgroundState: VoiceChatActionButtonBackgroundNode.State - var animated = true - switch state { - case let .button(text): - backgroundState = .button - self.buttonTitleLabel.alpha = 1.0 - self.buttonTitleLabel.attributedText = NSAttributedString(string: text, font: Font.semibold(17.0), textColor: .white) - let titleSize = self.buttonTitleLabel.updateLayout(CGSize(width: size.width, height: 100.0)) - self.buttonTitleLabel.frame = CGRect(origin: CGPoint(x: floor((self.bounds.width - titleSize.width) / 2.0), y: floor((self.bounds.height - titleSize.height) / 2.0)), size: titleSize) - case .scheduled: - backgroundState = .disabled - if previousState == .connecting { - animated = false - } - case let .active(state): - switch state { - case .on: - backgroundState = .blob(true) - case .muted: - backgroundState = .blob(false) - case .cantSpeak: - backgroundState = .disabled - } - case .connecting: - backgroundState = .connecting - } - self.applyIconParams() - - self.backgroundNode.glowHidden = (self.currentParams?.snap ?? false) || small - self.backgroundNode.isDark = dark - self.backgroundNode.update(state: backgroundState, animated: animated) - - if case .active = state, let previousState = previousState, case .connecting = previousState, animated { - self.activeDisposable.set((self.activePromise.get() - |> deliverOnMainQueue).start(next: { [weak self] active in - if active { - self?.activeDisposable.set(nil) - self?.applyParams(animated: true) - } - })) - } else { - self.applyParams(animated: animated) - } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - var hitRect = self.bounds - if let (_, buttonSize, state, _, _, _, _, _) = self.currentParams { - if case .button = state { - hitRect = CGRect(x: 0.0, y: floor((self.bounds.height - buttonHeight) / 2.0), width: self.bounds.width, height: buttonHeight) - } else { - hitRect = self.bounds.insetBy(dx: (self.bounds.width - buttonSize.width) / 2.0, dy: (self.bounds.height - buttonSize.height) / 2.0) - } - } - let result = super.hitTest(point, with: event) - if !hitRect.contains(point) { - return nil - } - return result - } - - func playAnimation() { - self.iconNode.playRandomAnimation() - } -} - -extension UIBezierPath { - static func smoothCurve(through points: [CGPoint], length: CGFloat, smoothness: CGFloat, curve: Bool = false) -> UIBezierPath { - var smoothPoints = [SmoothPoint]() - for index in (0 ..< points.count) { - let prevIndex = index - 1 - let prev = points[prevIndex >= 0 ? prevIndex : points.count + prevIndex] - let curr = points[index] - let next = points[(index + 1) % points.count] - - let angle: CGFloat = { - let dx = next.x - prev.x - let dy = -next.y + prev.y - let angle = atan2(dy, dx) - if angle < 0 { - return abs(angle) - } else { - return 2 * .pi - angle - } - }() - - smoothPoints.append( - SmoothPoint( - point: curr, - inAngle: angle + .pi, - inLength: smoothness * distance(from: curr, to: prev), - outAngle: angle, - outLength: smoothness * distance(from: curr, to: next) - ) - ) - } - - let resultPath = UIBezierPath() - if curve { - resultPath.move(to: CGPoint()) - resultPath.addLine(to: smoothPoints[0].point) - } else { - resultPath.move(to: smoothPoints[0].point) - } - - let smoothCount = curve ? smoothPoints.count - 1 : smoothPoints.count - for index in (0 ..< smoothCount) { - let curr = smoothPoints[index] - let next = smoothPoints[(index + 1) % points.count] - let currSmoothOut = curr.smoothOut() - let nextSmoothIn = next.smoothIn() - resultPath.addCurve(to: next.point, controlPoint1: currSmoothOut, controlPoint2: nextSmoothIn) - } - if curve { - resultPath.addLine(to: CGPoint(x: length, y: 0.0)) - } - resultPath.close() - return resultPath - } - - static private func distance(from fromPoint: CGPoint, to toPoint: CGPoint) -> CGFloat { - return sqrt((fromPoint.x - toPoint.x) * (fromPoint.x - toPoint.x) + (fromPoint.y - toPoint.y) * (fromPoint.y - toPoint.y)) - } - - struct SmoothPoint { - let point: CGPoint - - let inAngle: CGFloat - let inLength: CGFloat - - let outAngle: CGFloat - let outLength: CGFloat - - func smoothIn() -> CGPoint { - return smooth(angle: inAngle, length: inLength) - } - - func smoothOut() -> CGPoint { - return smooth(angle: outAngle, length: outLength) - } - - private func smooth(angle: CGFloat, length: CGFloat) -> CGPoint { - return CGPoint( - x: point.x + length * cos(angle), - y: point.y + length * sin(angle) - ) - } - } -} - -private let progressLineWidth: CGFloat = 3.0 + UIScreenPixel -private let buttonSize = CGSize(width: 112.0, height: 112.0) -private let radius = buttonSize.width / 2.0 - -private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode { - enum State: Equatable { - case connecting - case disabled - case button - case blob(Bool) - } - - private var state: State - private var hasState = false - - private var transition: State? - - var audioLevel: CGFloat = 0.0 { - didSet { - self.maskBlobView.updateLevel(self.audioLevel, immediately: false) - } - } - - - - var updatedActive: ((Bool) -> Void)? - var updatedColors: ((UIColor?, UIColor?) -> Void)? - - private let backgroundCircleLayer = CAShapeLayer() - private let foregroundCircleLayer = CAShapeLayer() - private let growingForegroundCircleLayer = CAShapeLayer() - - private let foregroundView = UIView() - private let foregroundGradientLayer = CAGradientLayer() - - private let maskView = UIView() - private let maskGradientLayer = CAGradientLayer() - private let maskBlobView: VoiceBlobView - private let maskCircleLayer = CAShapeLayer() - - fileprivate let maskProgressLayer = CAShapeLayer() - - private let maskMediumBlobLayer = CAShapeLayer() - private let maskBigBlobLayer = CAShapeLayer() - - private let hierarchyTrackingNode: HierarchyTrackingNode - private var isCurrentlyInHierarchy = false - var ignoreHierarchyChanges = false - - override init() { - self.state = .connecting - - self.maskBlobView = VoiceBlobView(frame: CGRect(origin: CGPoint(x: (areaSize.width - blobSize.width) / 2.0, y: (areaSize.height - blobSize.height) / 2.0), size: blobSize), maxLevel: 1.5, mediumBlobRange: (0.69, 0.87), bigBlobRange: (0.71, 1.0)) - self.maskBlobView.setColor(white) - self.maskBlobView.isHidden = true - - var updateInHierarchy: ((Bool) -> Void)? - self.hierarchyTrackingNode = HierarchyTrackingNode({ value in - updateInHierarchy?(value) - }) - - super.init() - - self.addSubnode(self.hierarchyTrackingNode) - - let circlePath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: buttonSize)).cgPath - self.backgroundCircleLayer.fillColor = greyColor.cgColor - self.backgroundCircleLayer.path = circlePath - - let smallerCirclePath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: CGSize(width: buttonSize.width - progressLineWidth, height: buttonSize.height - progressLineWidth))).cgPath - self.foregroundCircleLayer.fillColor = greyColor.cgColor - self.foregroundCircleLayer.path = smallerCirclePath - self.foregroundCircleLayer.transform = CATransform3DMakeScale(0.0, 0.0, 1) - self.foregroundCircleLayer.isHidden = true - - self.growingForegroundCircleLayer.fillColor = greyColor.cgColor - self.growingForegroundCircleLayer.path = smallerCirclePath - self.growingForegroundCircleLayer.transform = CATransform3DMakeScale(1.0, 1.0, 1) - self.growingForegroundCircleLayer.isHidden = true - - self.foregroundGradientLayer.type = .radial - self.foregroundGradientLayer.colors = [lightBlue.cgColor, blue.cgColor, blue.cgColor] - self.foregroundGradientLayer.locations = [0.0, 0.55, 1.0] - self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0) - self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0) - - self.maskView.backgroundColor = .clear - - self.maskGradientLayer.type = .radial - self.maskGradientLayer.colors = [UIColor(rgb: 0xffffff, alpha: 0.4).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor] - self.maskGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5) - self.maskGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0) - self.maskGradientLayer.transform = CATransform3DMakeScale(0.3, 0.3, 1.0) - self.maskGradientLayer.isHidden = true - - let path = CGMutablePath() - path.addArc(center: CGPoint(x: (buttonSize.width + 6.0) / 2.0, y: (buttonSize.height + 6.0) / 2.0), radius: radius, startAngle: 0.0, endAngle: CGFloat.pi * 2.0, clockwise: true) - - self.maskProgressLayer.strokeColor = white.cgColor - self.maskProgressLayer.fillColor = UIColor.clear.cgColor - self.maskProgressLayer.lineWidth = progressLineWidth - self.maskProgressLayer.lineCap = .round - self.maskProgressLayer.path = path - - let circleFrame = CGRect(origin: CGPoint(x: (areaSize.width - buttonSize.width) / 2.0, y: (areaSize.height - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0) - let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath - - self.maskCircleLayer.path = largerCirclePath - self.maskCircleLayer.fillColor = white.cgColor - self.maskCircleLayer.isHidden = true - - updateInHierarchy = { [weak self] value in - if let strongSelf = self, !strongSelf.ignoreHierarchyChanges { - strongSelf.isCurrentlyInHierarchy = value - strongSelf.updateAnimations() - } - } - } - - override func didLoad() { - super.didLoad() - - self.layer.addSublayer(self.backgroundCircleLayer) - - self.view.addSubview(self.foregroundView) - self.layer.addSublayer(self.foregroundCircleLayer) - self.layer.addSublayer(self.growingForegroundCircleLayer) - - self.foregroundView.mask = self.maskView - self.foregroundView.layer.addSublayer(self.foregroundGradientLayer) - - self.maskView.layer.addSublayer(self.maskGradientLayer) - self.maskView.layer.addSublayer(self.maskProgressLayer) - self.maskView.addSubview(self.maskBlobView) - self.maskView.layer.addSublayer(self.maskCircleLayer) - - self.maskBlobView.scaleUpdated = { [weak self] scale in - if let strongSelf = self { - strongSelf.updateGlowScale(strongSelf.isActive ? scale : nil) - } - } - } - - private func setupGradientAnimations() { - if let _ = self.foregroundGradientLayer.animation(forKey: "movement") { - } else { - let previousValue = self.foregroundGradientLayer.startPoint - let newValue: CGPoint - if self.maskBlobView.presentationAudioLevel > 0.22 { - newValue = CGPoint(x: CGFloat.random(in: 0.9 ..< 1.0), y: CGFloat.random(in: 0.15 ..< 0.35)) - } else if self.maskBlobView.presentationAudioLevel > 0.01 { - newValue = CGPoint(x: CGFloat.random(in: 0.57 ..< 0.85), y: CGFloat.random(in: 0.15 ..< 0.45)) - } else { - newValue = CGPoint(x: CGFloat.random(in: 0.6 ..< 0.75), y: CGFloat.random(in: 0.25 ..< 0.45)) - } - self.foregroundGradientLayer.startPoint = newValue - - CATransaction.begin() - - let animation = CABasicAnimation(keyPath: "startPoint") - animation.duration = Double.random(in: 0.8 ..< 1.4) - animation.fromValue = previousValue - animation.toValue = newValue - - CATransaction.setCompletionBlock { [weak self] in - if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy { - self?.setupGradientAnimations() - } - } - - self.foregroundGradientLayer.add(animation, forKey: "movement") - CATransaction.commit() - } - } - - private func setupProgressAnimations() { - if let _ = self.maskProgressLayer.animation(forKey: "progressRotation") { - } else { - self.maskProgressLayer.isHidden = false - - let animation = CABasicAnimation(keyPath: "transform.rotation.z") - animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) - animation.duration = 1.0 - animation.fromValue = NSNumber(value: Float(0.0)) - animation.toValue = NSNumber(value: Float.pi * 2.0) - animation.repeatCount = Float.infinity - animation.beginTime = 0.0 - self.maskProgressLayer.add(animation, forKey: "progressRotation") - - let shrinkAnimation = CABasicAnimation(keyPath: "strokeEnd") - shrinkAnimation.fromValue = 1.0 - shrinkAnimation.toValue = 0.0 - shrinkAnimation.duration = 1.0 - shrinkAnimation.beginTime = 0.0 - - let growthAnimation = CABasicAnimation(keyPath: "strokeEnd") - growthAnimation.fromValue = 0.0 - growthAnimation.toValue = 1.0 - growthAnimation.duration = 1.0 - growthAnimation.beginTime = 1.0 - - let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z") - rotateAnimation.fromValue = 0.0 - rotateAnimation.toValue = CGFloat.pi * 2 - rotateAnimation.isAdditive = true - rotateAnimation.duration = 1.0 - rotateAnimation.beginTime = 1.0 - - let groupAnimation = CAAnimationGroup() - groupAnimation.repeatCount = Float.infinity - groupAnimation.animations = [shrinkAnimation, growthAnimation, rotateAnimation] - groupAnimation.duration = 2.0 - - self.maskProgressLayer.add(groupAnimation, forKey: "progressGrowth") - } - } - - var glowHidden: Bool = false { - didSet { - if self.glowHidden != oldValue { - let initialAlpha = CGFloat(self.maskProgressLayer.opacity) - let targetAlpha: CGFloat = self.glowHidden ? 0.0 : 1.0 - self.maskGradientLayer.opacity = Float(targetAlpha) - self.maskGradientLayer.animateAlpha(from: initialAlpha, to: targetAlpha, duration: 0.2) - } - } - } - - var disableGlowAnimations = false - func updateGlowScale(_ scale: CGFloat?) { - if self.disableGlowAnimations { - return - } - if let scale = scale { - self.maskGradientLayer.transform = CATransform3DMakeScale(0.89 + 0.11 * scale, 0.89 + 0.11 * scale, 1.0) - } else { - let initialScale: CGFloat = ((self.maskGradientLayer.value(forKeyPath: "presentationLayer.transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? (((self.maskGradientLayer.value(forKeyPath: "transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? (0.89)) - let targetScale: CGFloat = self.isActive ? 0.89 : 0.85 - if abs(targetScale - initialScale) > 0.03 { - self.maskGradientLayer.transform = CATransform3DMakeScale(targetScale, targetScale, 1.0) - self.maskGradientLayer.animateScale(from: initialScale, to: targetScale, duration: 0.3) - } - } - } - - enum Gradient { - case speaking - case active - case connecting - case muted - } - - func updateGlowAndGradientAnimations(type: Gradient, previousType: Gradient? = nil, animated: Bool = true) { - let effectivePreviousTyoe = previousType ?? .active - - let scale: CGFloat - if case .speaking = effectivePreviousTyoe { - scale = 0.95 - } else { - scale = 0.8 - } - - let initialScale: CGFloat = ((self.maskGradientLayer.value(forKeyPath: "presentationLayer.transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? (((self.maskGradientLayer.value(forKeyPath: "transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? scale) - let initialColors = self.foregroundGradientLayer.colors - - let outerColor: UIColor? - let activeColor: UIColor? - let targetColors: [CGColor] - let targetScale: CGFloat - switch type { - case .speaking: - targetColors = [activeBlue.cgColor, green.cgColor, green.cgColor] - targetScale = 0.89 - outerColor = UIColor(rgb: 0x134b22) - activeColor = green - case .active: - targetColors = [lightBlue.cgColor, blue.cgColor, blue.cgColor] - targetScale = 0.85 - outerColor = UIColor(rgb: 0x002e5d) - activeColor = blue - case .connecting: - targetColors = [lightBlue.cgColor, blue.cgColor, blue.cgColor] - targetScale = 0.3 - outerColor = nil - activeColor = blue - case .muted: - targetColors = [pink.cgColor, purple.cgColor, purple.cgColor] - targetScale = 0.85 - outerColor = UIColor(rgb: 0x24306b) - activeColor = purple - } - self.updatedColors?(outerColor, activeColor) - - self.maskGradientLayer.transform = CATransform3DMakeScale(targetScale, targetScale, 1.0) - if let _ = previousType { - self.maskGradientLayer.animateScale(from: initialScale, to: targetScale, duration: 0.3) - } else if animated { - self.maskGradientLayer.animateSpring(from: initialScale as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", duration: 0.45) - } - - self.foregroundGradientLayer.colors = targetColors - if animated { - self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) - } - } - - private func playMuteAnimation() { - if self.animationsEnabled { - self.maskBlobView.startAnimating() - } - self.maskBlobView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak self] _ in - guard let strongSelf = self else { - return - } - if strongSelf.state != .connecting { - return - } - strongSelf.maskBlobView.isHidden = true - strongSelf.maskBlobView.stopAnimating() - strongSelf.maskBlobView.layer.removeAllAnimations() - }) - } - - var animatingDisappearance = false - private func playDeactivationAnimation() { - if self.animatingDisappearance { - return - } - self.animatingDisappearance = true - CATransaction.begin() - CATransaction.setDisableActions(true) - self.growingForegroundCircleLayer.isHidden = false - CATransaction.commit() - - self.disableGlowAnimations = true - self.maskGradientLayer.removeAllAnimations() - self.updateGlowAndGradientAnimations(type: .connecting, previousType: nil) - - if self.animationsEnabled { - self.maskBlobView.startAnimating() - } - self.maskBlobView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak self] _ in - guard let strongSelf = self else { - return - } - if strongSelf.state != .connecting { - return - } - strongSelf.maskBlobView.isHidden = true - strongSelf.maskBlobView.stopAnimating() - strongSelf.maskBlobView.layer.removeAllAnimations() - }) - - CATransaction.begin() - let growthAnimation = CABasicAnimation(keyPath: "transform.scale") - growthAnimation.fromValue = 0.0 - growthAnimation.toValue = 1.0 - growthAnimation.duration = 0.15 - growthAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) - growthAnimation.isRemovedOnCompletion = false - growthAnimation.fillMode = .forwards - - CATransaction.setCompletionBlock { - self.animatingDisappearance = false - self.growingForegroundCircleLayer.isHidden = true - self.disableGlowAnimations = false - if self.state != .connecting { - return - } - CATransaction.begin() - CATransaction.setDisableActions(true) - self.maskGradientLayer.isHidden = true - self.maskCircleLayer.isHidden = true - self.growingForegroundCircleLayer.removeAllAnimations() - CATransaction.commit() - } - - self.growingForegroundCircleLayer.add(growthAnimation, forKey: "insideGrowth") - CATransaction.commit() - } - - private func playActivationAnimation(active: Bool) { - CATransaction.begin() - CATransaction.setDisableActions(true) - self.maskCircleLayer.isHidden = false - self.maskProgressLayer.isHidden = true - self.maskGradientLayer.isHidden = false - CATransaction.commit() - - self.maskGradientLayer.removeAllAnimations() - self.updateGlowAndGradientAnimations(type: active ? .speaking : .active, previousType: nil) - - self.maskBlobView.isHidden = false - if self.animationsEnabled { - self.maskBlobView.startAnimating() - } - self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) - } - - private func playConnectionAnimation(type: Gradient, completion: @escaping () -> Void) { - CATransaction.begin() - let initialRotation: CGFloat = CGFloat((self.maskProgressLayer.value(forKeyPath: "presentationLayer.transform.rotation.z") as? NSNumber)?.floatValue ?? 0.0) - let initialStrokeEnd: CGFloat = CGFloat((self.maskProgressLayer.value(forKeyPath: "presentationLayer.strokeEnd") as? NSNumber)?.floatValue ?? 1.0) - - self.maskProgressLayer.removeAnimation(forKey: "progressGrowth") - self.maskProgressLayer.removeAnimation(forKey: "progressRotation") - - let duration: Double = (1.0 - Double(initialStrokeEnd)) * 0.3 - - let growthAnimation = CABasicAnimation(keyPath: "strokeEnd") - growthAnimation.fromValue = initialStrokeEnd - growthAnimation.toValue = 1.0 - growthAnimation.duration = duration - growthAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn) - - let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z") - rotateAnimation.fromValue = initialRotation - rotateAnimation.toValue = initialRotation + CGFloat.pi * 2 - rotateAnimation.isAdditive = true - rotateAnimation.duration = duration - rotateAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn) - - let groupAnimation = CAAnimationGroup() - groupAnimation.animations = [growthAnimation, rotateAnimation] - groupAnimation.duration = duration - - CATransaction.setCompletionBlock { - var active = true - if case .connecting = self.state { - active = false - } - if active { - CATransaction.begin() - CATransaction.setDisableActions(true) - self.foregroundCircleLayer.isHidden = false - self.foregroundCircleLayer.transform = CATransform3DMakeScale(1.0, 1.0, 1.0) - self.maskCircleLayer.isHidden = false - self.maskProgressLayer.isHidden = true - self.maskGradientLayer.isHidden = false - CATransaction.commit() - - completion() - - self.updateGlowAndGradientAnimations(type: type, previousType: nil) - - if case .connecting = self.state { - } else { - self.maskBlobView.isHidden = false - if self.animationsEnabled { - self.maskBlobView.startAnimating() - } - self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) - } - - self.updatedActive?(true) - - CATransaction.begin() - let shrinkAnimation = CABasicAnimation(keyPath: "transform.scale") - shrinkAnimation.fromValue = 1.0 - shrinkAnimation.toValue = 0.00001 - shrinkAnimation.duration = 0.15 - shrinkAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn) - shrinkAnimation.isRemovedOnCompletion = false - shrinkAnimation.fillMode = .forwards - - CATransaction.setCompletionBlock { - CATransaction.begin() - CATransaction.setDisableActions(true) - self.foregroundCircleLayer.isHidden = true - self.foregroundCircleLayer.transform = CATransform3DMakeScale(0.0, 0.0, 1.0) - self.foregroundCircleLayer.removeAllAnimations() - CATransaction.commit() - } - - self.foregroundCircleLayer.add(shrinkAnimation, forKey: "insideShrink") - CATransaction.commit() - } - } - - self.maskProgressLayer.add(groupAnimation, forKey: "progressCompletion") - CATransaction.commit() - } - - private var maskIsCircle = true - private func setupButtonAnimation() { - CATransaction.begin() - CATransaction.setDisableActions(true) - self.backgroundCircleLayer.isHidden = true - self.foregroundCircleLayer.isHidden = true - self.maskCircleLayer.isHidden = false - self.maskProgressLayer.isHidden = true - self.maskGradientLayer.isHidden = true - - let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: floor((self.bounds.height - buttonHeight) / 2.0), width: self.bounds.width, height: buttonHeight), cornerRadius: 10.0).cgPath - self.maskCircleLayer.path = path - self.maskIsCircle = false - - CATransaction.commit() - - self.updateGlowAndGradientAnimations(type: .muted, previousType: nil) - - self.updatedActive?(true) - } - - private func playScheduledAnimation() { - CATransaction.begin() - CATransaction.setDisableActions(true) - self.maskGradientLayer.isHidden = false - CATransaction.commit() - - let circleFrame = CGRect(origin: CGPoint(x: (self.bounds.width - buttonSize.width) / 2.0, y: (self.bounds.height - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0) - let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath - - let previousPath = self.maskCircleLayer.path - self.maskCircleLayer.path = largerCirclePath - self.maskIsCircle = true - - self.maskCircleLayer.animateSpring(from: previousPath as AnyObject, to: largerCirclePath as AnyObject, keyPath: "path", duration: 0.6, initialVelocity: 0.0, damping: 100.0) - - self.maskBlobView.isHidden = false - if self.animationsEnabled { - self.maskBlobView.startAnimating() - } - self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, damping: 100.0) - - self.disableGlowAnimations = true - self.maskGradientLayer.removeAllAnimations() - self.maskGradientLayer.animateSpring(from: 0.3 as NSNumber, to: 0.85 as NSNumber, keyPath: "transform.scale", duration: 0.45, completion: { [weak self] _ in - self?.disableGlowAnimations = false - }) - } - - var animationsEnabled: Bool = true { - didSet { - self.updateAnimations() - } - } - - var isActive = false - func updateAnimations() { - if !self.isCurrentlyInHierarchy { - self.foregroundGradientLayer.removeAllAnimations() - self.growingForegroundCircleLayer.removeAllAnimations() - self.maskGradientLayer.removeAllAnimations() - self.maskProgressLayer.removeAllAnimations() - self.maskBlobView.stopAnimating() - return - } - - if !self.animationsEnabled { - self.foregroundGradientLayer.removeAllAnimations() - self.maskBlobView.stopAnimating() - } else { - self.setupGradientAnimations() - } - - switch self.state { - case .connecting: - self.updatedActive?(false) - if let transition = self.transition { - self.updateGlowScale(nil) - if case .blob = transition { - self.playDeactivationAnimation() - } else if case .disabled = transition { - self.playDeactivationAnimation() - } - self.transition = nil - } - self.setupProgressAnimations() - self.isActive = false - case let .blob(newActive): - if let transition = self.transition { - let type: Gradient = newActive ? .speaking : .active - if transition == .connecting { - self.playConnectionAnimation(type: type) { [weak self] in - self?.isActive = newActive - } - } else if transition == .disabled { - self.playActivationAnimation(active: newActive) - self.transition = nil - self.isActive = newActive - self.updatedActive?(true) - } else if case let .blob(previousActive) = transition { - self.updateGlowAndGradientAnimations(type: type, previousType: previousActive ? .speaking : .active) - self.transition = nil - self.isActive = newActive - } - self.transition = nil - } else { - if self.animationsEnabled { - self.maskBlobView.startAnimating() - } - } - case .disabled: - self.updatedActive?(true) - self.isActive = false - - if let transition = self.transition { - if case .button = transition { - self.playScheduledAnimation() - } else if case .connecting = transition { - self.playConnectionAnimation(type: .muted) { [weak self] in - self?.isActive = false - } - } else if case let .blob(previousActive) = transition { - self.updateGlowAndGradientAnimations(type: .muted, previousType: previousActive ? .speaking : .active) - self.playMuteAnimation() - } - self.transition = nil - } else { - if self.maskBlobView.isHidden { - self.updateGlowAndGradientAnimations(type: .muted, previousType: nil, animated: false) - self.maskCircleLayer.isHidden = false - self.maskProgressLayer.isHidden = true - self.maskGradientLayer.isHidden = false - self.maskBlobView.isHidden = false - if self.animationsEnabled { - self.maskBlobView.startAnimating() - } - self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) - } - } - case .button: - self.updatedActive?(true) - self.isActive = false - self.setupButtonAnimation() - } - } - - var isDark: Bool = false { - didSet { - if self.isDark != oldValue { - self.updateColors() - } - } - } - - var isSnap: Bool = false { - didSet { - if self.isSnap != oldValue { - self.updateColors() - } - } - } - - var connectingColor: UIColor = UIColor(rgb: 0xb6b6bb) { - didSet { - if self.connectingColor.rgb != oldValue.rgb { - self.updateColors() - } - } - } - - fileprivate func updateColors() { - let previousColor: CGColor = self.backgroundCircleLayer.fillColor ?? greyColor.cgColor - let targetColor: CGColor - if self.isSnap { - targetColor = self.connectingColor.cgColor - } else if self.isDark { - targetColor = secondaryGreyColor.cgColor - } else { - targetColor = greyColor.cgColor - } - self.backgroundCircleLayer.fillColor = targetColor - self.foregroundCircleLayer.fillColor = targetColor - self.growingForegroundCircleLayer.fillColor = targetColor - self.backgroundCircleLayer.animate(from: previousColor, to: targetColor, keyPath: "fillColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) - self.foregroundCircleLayer.animate(from: previousColor, to: targetColor, keyPath: "fillColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) - self.growingForegroundCircleLayer.animate(from: previousColor, to: targetColor, keyPath: "fillColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) - } - - func update(state: State, animated: Bool) { - var animated = animated - var hadState = true - if !self.hasState { - hadState = false - self.hasState = true - animated = false - } - - if state != self.state || !hadState { - if animated { - self.transition = self.state - } - self.state = state - } - - self.updateAnimations() - } - - var previousSize: CGSize? - override func layout() { - super.layout() - - let sizeUpdated = self.previousSize != self.bounds.size - self.previousSize = self.bounds.size - - let bounds = CGRect(x: (self.bounds.width - areaSize.width) / 2.0, y: (self.bounds.height - areaSize.height) / 2.0, width: areaSize.width, height: areaSize.height) - let center = bounds.center - - self.maskBlobView.frame = CGRect(origin: CGPoint(x: bounds.minX + (bounds.width - blobSize.width) / 2.0, y: bounds.minY + (bounds.height - blobSize.height) / 2.0), size: blobSize) - - let circleFrame = CGRect(origin: CGPoint(x: bounds.minX + (bounds.width - buttonSize.width) / 2.0, y: bounds.minY + (bounds.height - buttonSize.height) / 2.0), size: buttonSize) - self.backgroundCircleLayer.frame = circleFrame - self.foregroundCircleLayer.position = center - self.foregroundCircleLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: circleFrame.width - progressLineWidth, height: circleFrame.height - progressLineWidth)) - self.growingForegroundCircleLayer.position = center - self.growingForegroundCircleLayer.bounds = self.foregroundCircleLayer.bounds - self.maskCircleLayer.frame = self.bounds - - if sizeUpdated && self.maskIsCircle { - CATransaction.begin() - CATransaction.setDisableActions(true) - let circleFrame = CGRect(origin: CGPoint(x: (self.bounds.width - buttonSize.width) / 2.0, y: (self.bounds.height - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0) - let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath - - self.maskCircleLayer.path = largerCirclePath - CATransaction.commit() - } - - self.maskProgressLayer.frame = circleFrame.insetBy(dx: -3.0, dy: -3.0) - self.foregroundView.frame = self.bounds - self.foregroundGradientLayer.frame = self.bounds - self.maskGradientLayer.position = center - self.maskGradientLayer.bounds = bounds - self.maskView.frame = self.bounds - } -} - -private final class VoiceBlobView: UIView { - private let mediumBlob: BlobView - private let bigBlob: BlobView - - private let maxLevel: CGFloat - - private var displayLinkAnimator: ConstantDisplayLinkAnimator? - - private var audioLevel: CGFloat = 0.0 - var presentationAudioLevel: CGFloat = 0.0 - - var scaleUpdated: ((CGFloat) -> Void)? { - didSet { - self.bigBlob.scaleUpdated = self.scaleUpdated - } - } - - private(set) var isAnimating = false - - public typealias BlobRange = (min: CGFloat, max: CGFloat) - - private let hierarchyTrackingNode: HierarchyTrackingNode - private var isCurrentlyInHierarchy = true - - public init( - frame: CGRect, - maxLevel: CGFloat, - mediumBlobRange: BlobRange, - bigBlobRange: BlobRange - ) { - var updateInHierarchy: ((Bool) -> Void)? - self.hierarchyTrackingNode = HierarchyTrackingNode({ value in - updateInHierarchy?(value) - }) - - self.maxLevel = maxLevel - - self.mediumBlob = BlobView( - pointsCount: 8, - minRandomness: 1, - maxRandomness: 1, - minSpeed: 0.9, - maxSpeed: 4.0, - minScale: mediumBlobRange.min, - maxScale: mediumBlobRange.max - ) - self.bigBlob = BlobView( - pointsCount: 8, - minRandomness: 1, - maxRandomness: 1, - minSpeed: 1.0, - maxSpeed: 4.4, - minScale: bigBlobRange.min, - maxScale: bigBlobRange.max - ) - - super.init(frame: frame) - - self.addSubnode(self.hierarchyTrackingNode) - - self.addSubview(self.bigBlob) - self.addSubview(self.mediumBlob) - - self.displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in - guard let strongSelf = self else { return } - - if !strongSelf.isCurrentlyInHierarchy { - return - } - - strongSelf.presentationAudioLevel = strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1 - - strongSelf.mediumBlob.level = strongSelf.presentationAudioLevel - strongSelf.bigBlob.level = strongSelf.presentationAudioLevel - } - - updateInHierarchy = { [weak self] value in - if let strongSelf = self { - strongSelf.isCurrentlyInHierarchy = value - } - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public func setColor(_ color: UIColor) { - self.mediumBlob.setColor(color.withAlphaComponent(0.5)) - self.bigBlob.setColor(color.withAlphaComponent(0.21)) - } - - public func updateLevel(_ level: CGFloat, immediately: Bool) { - let normalizedLevel = min(1, max(level / maxLevel, 0)) - - self.mediumBlob.updateSpeedLevel(to: normalizedLevel) - self.bigBlob.updateSpeedLevel(to: normalizedLevel) - - self.audioLevel = normalizedLevel - if immediately { - self.presentationAudioLevel = normalizedLevel - } - } - - public func startAnimating() { - guard !self.isAnimating else { return } - self.isAnimating = true - - self.updateBlobsState() - - self.displayLinkAnimator?.isPaused = false - } - - public func stopAnimating() { - self.stopAnimating(duration: 0.15) - } - - public func stopAnimating(duration: Double) { - guard isAnimating else { return } - self.isAnimating = false - - self.updateBlobsState() - - self.displayLinkAnimator?.isPaused = true - } - - private func updateBlobsState() { - if self.isAnimating { - if self.mediumBlob.frame.size != .zero { - self.mediumBlob.startAnimating() - self.bigBlob.startAnimating() - } - } else { - self.mediumBlob.stopAnimating() - self.bigBlob.stopAnimating() - } - } - - override public func layoutSubviews() { - super.layoutSubviews() - - self.mediumBlob.frame = bounds - self.bigBlob.frame = bounds - - self.updateBlobsState() - } -} - -final class BlobView: UIView { - let pointsCount: Int - let smoothness: CGFloat - - let minRandomness: CGFloat - let maxRandomness: CGFloat - - let minSpeed: CGFloat - let maxSpeed: CGFloat - - let minScale: CGFloat - let maxScale: CGFloat - - var scaleUpdated: ((CGFloat) -> Void)? - - var level: CGFloat = 0 { - didSet { - if abs(self.level - oldValue) > 0.01 { - CATransaction.begin() - CATransaction.setDisableActions(true) - let lv = self.minScale + (self.maxScale - self.minScale) * self.level - self.shapeLayer.transform = CATransform3DMakeScale(lv, lv, 1) - self.scaleUpdated?(self.level) - CATransaction.commit() - } - } - } - - private var speedLevel: CGFloat = 0 - private var lastSpeedLevel: CGFloat = 0 - - private let shapeLayer: CAShapeLayer = { - let layer = CAShapeLayer() - layer.strokeColor = nil - return layer - }() - - init( - pointsCount: Int, - minRandomness: CGFloat, - maxRandomness: CGFloat, - minSpeed: CGFloat, - maxSpeed: CGFloat, - minScale: CGFloat, - maxScale: CGFloat - ) { - self.pointsCount = pointsCount - self.minRandomness = minRandomness - self.maxRandomness = maxRandomness - self.minSpeed = minSpeed - self.maxSpeed = maxSpeed - self.minScale = minScale - self.maxScale = maxScale - - let angle = (CGFloat.pi * 2) / CGFloat(pointsCount) - self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2 - - super.init(frame: .zero) - - self.layer.addSublayer(self.shapeLayer) - - self.shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setColor(_ color: UIColor) { - self.shapeLayer.fillColor = color.cgColor - } - - func updateSpeedLevel(to newSpeedLevel: CGFloat) { - self.speedLevel = max(self.speedLevel, newSpeedLevel) - } - - func startAnimating() { - self.animateToNewShape() - } - - func stopAnimating() { - self.shapeLayer.removeAnimation(forKey: "path") - } - - private func animateToNewShape() { - if self.shapeLayer.path == nil { - let points = generateNextBlob(for: self.bounds.size) - self.shapeLayer.path = UIBezierPath.smoothCurve(through: points, length: bounds.width, smoothness: smoothness).cgPath - } - - let nextPoints = generateNextBlob(for: self.bounds.size) - let nextPath = UIBezierPath.smoothCurve(through: nextPoints, length: bounds.width, smoothness: smoothness).cgPath - - let animation = CABasicAnimation(keyPath: "path") - let previousPath = self.shapeLayer.path - self.shapeLayer.path = nextPath - animation.duration = CFTimeInterval(1.0 / (minSpeed + (maxSpeed - minSpeed) * speedLevel)) - animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) - animation.fromValue = previousPath - animation.toValue = nextPath - animation.isRemovedOnCompletion = false - animation.fillMode = .forwards - animation.completion = { [weak self] finished in - if finished { - self?.animateToNewShape() - } - } - - self.shapeLayer.add(animation, forKey: "path") - - self.lastSpeedLevel = self.speedLevel - self.speedLevel = 0 - } - - // MARK: Helpers - - private func generateNextBlob(for size: CGSize) -> [CGPoint] { - let randomness = minRandomness + (maxRandomness - minRandomness) * speedLevel - return blob(pointsCount: pointsCount, randomness: randomness) - .map { - return CGPoint( - x: $0.x * CGFloat(size.width), - y: $0.y * CGFloat(size.height) - ) - } - } - - func blob(pointsCount: Int, randomness: CGFloat) -> [CGPoint] { - let angle = (CGFloat.pi * 2) / CGFloat(pointsCount) - - let rgen = { () -> CGFloat in - let accuracy: UInt32 = 1000 - let random = arc4random_uniform(accuracy) - return CGFloat(random) / CGFloat(accuracy) - } - let rangeStart: CGFloat = 1 / (1 + randomness / 10) - - let startAngle = angle * CGFloat(arc4random_uniform(100)) / CGFloat(100) - - let points = (0 ..< pointsCount).map { i -> CGPoint in - let randPointOffset = (rangeStart + CGFloat(rgen()) * (1 - rangeStart)) / 2 - let angleRandomness: CGFloat = angle * 0.1 - let randAngle = angle + angle * ((angleRandomness * CGFloat(arc4random_uniform(100)) / CGFloat(100)) - angleRandomness * 0.5) - let pointX = sin(startAngle + CGFloat(i) * randAngle) - let pointY = cos(startAngle + CGFloat(i) * randAngle) - return CGPoint( - x: pointX * randPointOffset, - y: pointY * randPointOffset - ) - } - - return points - } - - override func layoutSubviews() { - super.layoutSubviews() - - CATransaction.begin() - CATransaction.setDisableActions(true) - self.shapeLayer.position = CGPoint(x: bounds.midX, y: bounds.midY) - CATransaction.commit() - } -} - -enum VoiceChatActionButtonIconAnimationState: Equatable { - case empty - case start - case subscribe - case unsubscribe - case unmute - case mute - case hand -} - -final class VoiceChatActionButtonIconNode: ManagedAnimationNode { - private let isColored: Bool - private var iconState: VoiceChatActionButtonIconAnimationState = .mute - - init(isColored: Bool) { - self.isColored = isColored - super.init(size: CGSize(width: 100.0, height: 100.0)) - - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.1)) - } - - func enqueueState(_ state: VoiceChatActionButtonIconAnimationState) { - guard self.iconState != state else { - return - } - - let previousState = self.iconState - self.iconState = state - - if state != .empty { - self.alpha = 1.0 - } - switch previousState { - case .empty: - switch state { - case .start: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001)) - default: - break - } - case .subscribe: - switch state { - case .unsubscribe: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceCancelReminder"))) - case .mute: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceSetReminderToMute"))) - case .hand: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceSetReminderToRaiseHand"))) - default: - break - } - case .unsubscribe: - switch state { - case .subscribe: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceSetReminder"))) - case .mute: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceCancelReminderToMute"))) - case .hand: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceCancelReminderToRaiseHand"))) - default: - break - } - case .start: - switch state { - case .mute: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart"))) - default: - break - } - case .unmute: - switch state { - case .mute: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceMute"))) - case .hand: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmuteToRaiseHand"))) - default: - break - } - case .mute: - switch state { - case .start: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001)) - case .unmute: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute"))) - case .hand: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceMuteToRaiseHand"))) - case .subscribe: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceSetReminderToRaiseHand"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001)) - case .unsubscribe: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceCancelReminderToRaiseHand"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001)) - case .empty: - self.alpha = 0.0 - default: - break - } - case .hand: - switch state { - case .mute, .unmute: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceRaiseHandToMute"))) - default: - break - } - } - } - - func playRandomAnimation() { - if case .hand = self.iconState { - if let next = self.trackStack.first, case let .local(name) = next.source, name.hasPrefix("VoiceHand_") { - return - } - - var useTiredAnimation = false - var useAngryAnimation = false - let val = Float.random(in: 0.0..<1.0) - if val <= 0.01 { - useTiredAnimation = true - } else if val <= 0.05 { - useAngryAnimation = true - } - - let normalAnimations = ["VoiceHand_1", "VoiceHand_2", "VoiceHand_3", "VoiceHand_4", "VoiceHand_7", "VoiceHand_8"] - let tiredAnimations = ["VoiceHand_5", "VoiceHand_6"] - let angryAnimations = ["VoiceHand_9", "VoiceHand_10"] - let animations: [String] - if useTiredAnimation { - animations = tiredAnimations - } else if useAngryAnimation { - animations = angryAnimations - } else { - animations = normalAnimations - } - if let animationName = animations.randomElement() { - self.trackTo(item: ManagedAnimationItem(source: .local(animationName))) - } - } - } -} - - -final class VoiceChatRaiseHandNode: ASDisplayNode { - private let animationNode: AnimationNode - private let color: UIColor? - private var playedOnce = false - - init(color: UIColor?) { - self.color = color - if let color = color, let url = getAppBundle().url(forResource: "anim_hand1", withExtension: "json"), let data = try? Data(contentsOf: url) { - self.animationNode = AnimationNode(animationData: transformedWithColors(data: data, colors: [(UIColor(rgb: 0xffffff), color)])) - } else { - self.animationNode = AnimationNode(animation: "anim_hand1", colors: nil, scale: 0.5) - } - super.init() - self.addSubnode(self.animationNode) - } - - func playRandomAnimation() { - guard self.playedOnce else { - self.playedOnce = true - self.animationNode.play() - return - } - - guard !self.animationNode.isPlaying else { - self.animationNode.completion = { [weak self] in - self?.playRandomAnimation() - } - return - } - - self.animationNode.completion = nil - if let animationName = ["anim_hand1", "anim_hand2", "anim_hand3", "anim_hand4"].randomElement() { - if let color = color, let url = getAppBundle().url(forResource: animationName, withExtension: "json"), let data = try? Data(contentsOf: url) { - self.animationNode.setAnimation(data: transformedWithColors(data: data, colors: [(UIColor(rgb: 0xffffff), color)])) - } else { - self.animationNode.setAnimation(name: animationName) - } - self.animationNode.play() - } - } - - override func layout() { - super.layout() - self.animationNode.frame = self.bounds - } -} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index d5844c0efdc..dc9a008717c 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -32,6 +32,7 @@ import MapResourceToAvatarSizes import SolidRoundedButtonNode import AudioBlob import DeviceAccess +import VoiceChatActionButton let panelBackgroundColor = UIColor(rgb: 0x1c1c1e) let secondaryPanelBackgroundColor = UIColor(rgb: 0x2c2c2e) @@ -2415,18 +2416,10 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } } else { if let input = (strongSelf.call as! PresentationGroupCallImpl).video(endpointId: endpointId) { - if let videoView = strongSelf.videoRenderingContext.makeView(input: input, blur: false) { - completion(GroupVideoNode(videoView: videoView, backdropVideoView: strongSelf.videoRenderingContext.makeView(input: input, blur: true))) + if let videoView = strongSelf.videoRenderingContext.makeView(input: input) { + completion(GroupVideoNode(videoView: videoView, backdropVideoView: strongSelf.videoRenderingContext.makeBlurView(input: input, mainView: videoView))) } } - - /*strongSelf.call.makeIncomingVideoView(endpointId: endpointId, requestClone: GroupVideoNode.useBlurTransparency, completion: { videoView, backdropVideoView in - if let videoView = videoView { - completion(GroupVideoNode(videoView: videoView, backdropVideoView: backdropVideoView)) - } else { - completion(nil) - } - })*/ } } } @@ -2459,6 +2452,23 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } }) } + + var lastTimestamp = 0.0 + self.call.onMutedSpeechActivityDetected = { [weak self] value in + Queue.mainQueue().async { + guard let self, value else { + return + } + let timestamp = CFAbsoluteTimeGetCurrent() + if lastTimestamp + 1000.0 < timestamp { + lastTimestamp = timestamp + + self.presentUndoOverlay(content: .info(title: nil, text: self.presentationData.strings.VoiceChat_ToastMicrophoneIsMuted, timeout: nil, customUndoText: nil), action: { _ in + return false + }) + } + } + } } deinit { @@ -2616,14 +2626,24 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } if !isScheduled && canSpeak { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_NoiseSuppression, textColor: .primary, textLayout: .secondLineWithValue(strongSelf.isNoiseSuppressionEnabled ? strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionEnabled : strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionDisabled), icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - f(.dismissWithoutContent) - if let strongSelf = self { - strongSelf.call.setIsNoiseSuppressionEnabled(!strongSelf.isNoiseSuppressionEnabled) - } - }))) + if #available(iOS 15.0, *) { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Microphone Modes", textColor: .primary, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.dismissWithoutContent) + AVCaptureDevice.showSystemUserInterface(.microphoneModes) + }))) + } else { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_NoiseSuppression, textColor: .primary, textLayout: .secondLineWithValue(strongSelf.isNoiseSuppressionEnabled ? strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionEnabled : strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionDisabled), icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.dismissWithoutContent) + if let strongSelf = self { + strongSelf.call.setIsNoiseSuppressionEnabled(!strongSelf.isNoiseSuppressionEnabled) + } + }))) + } } if let callState = strongSelf.callState, callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) { @@ -3717,7 +3737,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController var isFrontCamera = true let videoCapturer = OngoingCallVideoCapturer() let input = videoCapturer.video() - if let videoView = strongSelf.videoRenderingContext.makeView(input: input, blur: false) { + if let videoView = strongSelf.videoRenderingContext.makeView(input: input) { videoView.updateIsEnabled(true) let cameraNode = GroupVideoNode(videoView: videoView, backdropVideoView: nil) @@ -5488,8 +5508,8 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController self.requestedVideoSources.insert(channel.endpointId) let input = (self.call as! PresentationGroupCallImpl).video(endpointId: channel.endpointId) - if let input = input, let videoView = self.videoRenderingContext.makeView(input: input, blur: false) { - let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: self.videoRenderingContext.makeView(input: input, blur: true)) + if let input = input, let videoView = self.videoRenderingContext.makeView(input: input) { + let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: self.videoRenderingContext.makeBlurView(input: input, mainView: videoView)) self.readyVideoDisposables.set((combineLatest(videoNode.ready, .single(false) |> then(.single(true) |> delay(10.0, queue: Queue.mainQueue()))) |> deliverOnMainQueue @@ -5541,65 +5561,6 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController self.updateMembers() } } - - /*self.call.makeIncomingVideoView(endpointId: channel.endpointId, requestClone: GroupVideoNode.useBlurTransparency, completion: { [weak self] videoView, backdropVideoView in - Queue.mainQueue().async { - guard let strongSelf = self, let videoView = videoView else { - return - } - let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: backdropVideoView) - - strongSelf.readyVideoDisposables.set((combineLatest(videoNode.ready, .single(false) |> then(.single(true) |> delay(10.0, queue: Queue.mainQueue()))) - |> deliverOnMainQueue - ).start(next: { [weak self, weak videoNode] ready, timeouted in - if let strongSelf = self, let videoNode = videoNode { - Queue.mainQueue().after(0.1) { - if timeouted && !ready { - strongSelf.timeoutedEndpointIds.insert(channel.endpointId) - strongSelf.readyVideoEndpointIds.remove(channel.endpointId) - strongSelf.readyVideoEndpointIdsPromise.set(strongSelf.readyVideoEndpointIds) - strongSelf.wideVideoNodes.remove(channel.endpointId) - - strongSelf.updateMembers() - } else if ready { - strongSelf.readyVideoEndpointIds.insert(channel.endpointId) - strongSelf.readyVideoEndpointIdsPromise.set(strongSelf.readyVideoEndpointIds) - strongSelf.timeoutedEndpointIds.remove(channel.endpointId) - if videoNode.aspectRatio <= 0.77 { - strongSelf.wideVideoNodes.insert(channel.endpointId) - } else { - strongSelf.wideVideoNodes.remove(channel.endpointId) - } - strongSelf.updateMembers() - - if let (layout, _) = strongSelf.validLayout, case .compact = layout.metrics.widthClass { - if let interaction = strongSelf.itemInteraction { - loop: for i in 0 ..< strongSelf.currentFullscreenEntries.count { - let entry = strongSelf.currentFullscreenEntries[i] - switch entry { - case let .peer(peerEntry, _): - if peerEntry.effectiveVideoEndpointId == channel.endpointId { - let presentationData = strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme) - strongSelf.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.fullscreenItem(context: strongSelf.context, presentationData: presentationData, interaction: interaction), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil) - break loop - } - default: - break - } - } - } - } - } - } - } - }), forKey: channel.endpointId) - strongSelf.videoNodes[channel.endpointId] = videoNode - - if let _ = strongSelf.validLayout { - strongSelf.updateMembers() - } - } - })*/ } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift index c07a15d7050..34901095012 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift @@ -17,6 +17,7 @@ import AccountContext import LegacyComponents import AudioBlob import PeerInfoAvatarListNode +import VoiceChatActionButton private let avatarFont = avatarPlaceholderFont(size: floor(50.0 * 16.0 / 37.0)) private let tileSize = CGSize(width: 84.0, height: 84.0) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatOverlayController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatOverlayController.swift index 0f1b2e5ed4c..abec46a9e2a 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatOverlayController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatOverlayController.swift @@ -13,6 +13,7 @@ import AppBundle import ContextUI import PresentationDataUtils import TooltipUI +import VoiceChatActionButton private let slideOffset: CGFloat = 80.0 + 44.0 diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift index ee4c548c91f..057bab536db 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift @@ -17,6 +17,7 @@ import AudioBlob import PeerInfoAvatarListNode import ComponentFlow import EmojiStatusComponent +import VoiceChatActionButton final class VoiceChatParticipantItem: ListViewItem { enum ParticipantText: Equatable { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift index 40c399006af..e550fa3e71e 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift @@ -8,6 +8,7 @@ import AccountContext import TelegramPresentationData import SolidRoundedButtonNode import PresentationDataUtils +import VoiceChatActionButton private let accentColor: UIColor = UIColor(rgb: 0x007aff) diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index d52b1dc7b51..7163cb19947 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -120,7 +120,7 @@ enum AccountStateMutationOperation { case UpdateAttachMenuBots case UpdateAudioTranscription(messageId: MessageId, id: Int64, isPending: Bool, text: String) case UpdateConfig - case UpdateExtendedMedia(MessageId, Api.MessageExtendedMedia) + case UpdateExtendedMedia(MessageId, [Api.MessageExtendedMedia]) case ResetForumTopic(topicId: MessageId, data: StoreMessageHistoryThreadData, pts: Int32) case UpdateStory(peerId: PeerId, story: Api.StoryItem) case UpdateReadStories(peerId: PeerId, maxId: Int32) @@ -130,6 +130,7 @@ enum AccountStateMutationOperation { case UpdateWallpaper(peerId: PeerId, wallpaper: TelegramWallpaper?) case UpdateRevenueBalances(peerId: PeerId, balances: RevenueStats.Balances) case UpdateStarsBalance(peerId: PeerId, balance: Int64) + case UpdateStarsRevenueStatus(peerId: PeerId, status: StarsRevenueStats.Balances) } struct HoleFromPreviousState { @@ -651,7 +652,7 @@ struct AccountMutableState { self.addOperation(.UpdateConfig) } - mutating func updateExtendedMedia(_ messageId: MessageId, extendedMedia: Api.MessageExtendedMedia) { + mutating func updateExtendedMedia(_ messageId: MessageId, extendedMedia: [Api.MessageExtendedMedia]) { self.addOperation(.UpdateExtendedMedia(messageId, extendedMedia)) } @@ -683,9 +684,13 @@ struct AccountMutableState { self.addOperation(.UpdateStarsBalance(peerId: peerId, balance: balance)) } + mutating func updateStarsRevenueStatus(peerId: PeerId, status: StarsRevenueStats.Balances) { + self.addOperation(.UpdateStarsRevenueStatus(peerId: peerId, status: status)) + } + mutating func addOperation(_ operation: AccountStateMutationOperation) { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .UpdateWallpaper, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateRevenueBalances, .UpdateStarsBalance: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .UpdateWallpaper, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateRevenueBalances, .UpdateStarsBalance, .UpdateStarsRevenueStatus: break case let .AddMessages(messages, location): for message in messages { @@ -831,6 +836,7 @@ struct AccountReplayedFinalState { let isPremiumUpdated: Bool let updatedRevenueBalances: [PeerId: RevenueStats.Balances] let updatedStarsBalance: [PeerId: Int64] + let updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] } struct AccountFinalStateEvents { @@ -859,12 +865,13 @@ struct AccountFinalStateEvents { let isPremiumUpdated: Bool let updatedRevenueBalances: [PeerId: RevenueStats.Balances] let updatedStarsBalance: [PeerId: Int64] + let updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] var isEmpty: Bool { - return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedRevenueBalances.isEmpty && self.updatedStarsBalance.isEmpty + return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedRevenueBalances.isEmpty && self.updatedStarsBalance.isEmpty && self.updatedStarsRevenueStatus.isEmpty } - init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedRevenueBalances: [PeerId: RevenueStats.Balances] = [:], updatedStarsBalance: [PeerId: Int64] = [:]) { + init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedRevenueBalances: [PeerId: RevenueStats.Balances] = [:], updatedStarsBalance: [PeerId: Int64] = [:], updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:]) { self.addedIncomingMessageIds = addedIncomingMessageIds self.addedReactionEvents = addedReactionEvents self.wasScheduledMessageIds = wasScheduledMessageIds @@ -890,6 +897,7 @@ struct AccountFinalStateEvents { self.isPremiumUpdated = isPremiumUpdated self.updatedRevenueBalances = updatedRevenueBalances self.updatedStarsBalance = updatedStarsBalance + self.updatedStarsRevenueStatus = updatedStarsRevenueStatus } init(state: AccountReplayedFinalState) { @@ -918,6 +926,7 @@ struct AccountFinalStateEvents { self.isPremiumUpdated = state.isPremiumUpdated self.updatedRevenueBalances = state.updatedRevenueBalances self.updatedStarsBalance = state.updatedStarsBalance + self.updatedStarsRevenueStatus = state.updatedStarsRevenueStatus } func union(with other: AccountFinalStateEvents) -> AccountFinalStateEvents { @@ -947,6 +956,6 @@ struct AccountFinalStateEvents { let isPremiumUpdated = self.isPremiumUpdated || other.isPremiumUpdated - return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, storyUpdates: self.storyUpdates + other.storyUpdates, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }), updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: self.updatedRevenueBalances.merging(other.updatedRevenueBalances, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsBalance: self.updatedStarsBalance.merging(other.updatedStarsBalance, uniquingKeysWith: { lhs, _ in lhs })) + return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, storyUpdates: self.storyUpdates + other.storyUpdates, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }), updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: self.updatedRevenueBalances.merging(other.updatedRevenueBalances, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsBalance: self.updatedStarsBalance.merging(other.updatedStarsBalance, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsRevenueStatus: self.updatedStarsRevenueStatus.merging(other.updatedStarsRevenueStatus, uniquingKeysWith: { lhs, _ in lhs })) } } diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index 6e819bfe916..64b6d8a5e3d 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -302,6 +302,7 @@ private var declaredEncodables: Void = { declareEncodable(SynchronizeViewStoriesOperation.self, f: { SynchronizeViewStoriesOperation(decoder: $0) }) declareEncodable(SynchronizePeerStoriesOperation.self, f: { SynchronizePeerStoriesOperation(decoder: $0) }) declareEncodable(MapVenue.self, f: { MapVenue(decoder: $0) }) + declareEncodable(MapGeoAddress.self, f: { MapGeoAddress(decoder: $0) }) declareEncodable(TelegramMediaGiveaway.self, f: { TelegramMediaGiveaway(decoder: $0) }) declareEncodable(TelegramMediaGiveawayResults.self, f: { TelegramMediaGiveawayResults(decoder: $0) }) declareEncodable(WebpagePreviewMessageAttribute.self, f: { WebpagePreviewMessageAttribute(decoder: $0) }) @@ -311,6 +312,7 @@ private var declaredEncodables: Void = { declareEncodable(OutgoingQuickReplyMessageAttribute.self, f: { OutgoingQuickReplyMessageAttribute(decoder: $0) }) declareEncodable(EffectMessageAttribute.self, f: { EffectMessageAttribute(decoder: $0) }) declareEncodable(FactCheckMessageAttribute.self, f: { FactCheckMessageAttribute(decoder: $0) }) + declareEncodable(TelegramMediaPaidContent.self, f: { TelegramMediaPaidContent(decoder: $0) }) return }() diff --git a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift index 59c86d13bf5..a1911091afe 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift @@ -94,7 +94,28 @@ public func mergedMessageReactionsAndPeers(accountPeerId: EnginePeer.Id, account } } - return (attribute.reactions, recentPeers) + #if DEBUG + var reactions = attribute.reactions + if "".isEmpty { + if let index = reactions.firstIndex(where: { + if case .custom(MessageReaction.starsReactionId) = $0.value { + return true + } else { + return false + } + }) { + let value = reactions[index] + reactions.remove(at: index) + reactions.insert(value, at: 0) + } else { + reactions.insert(MessageReaction(value: .custom(MessageReaction.starsReactionId), count: 1000000, chosenOrder: nil), at: 0) + } + } + #else + let reactions = attribute.reactions + #endif + + return (reactions, recentPeers) } private func mergeReactions(reactions: [MessageReaction], recentPeers: [ReactionsMessageAttribute.RecentPeer], pending: [PendingReactionsMessageAttribute.PendingReaction], accountPeerId: PeerId) -> ([MessageReaction], [ReactionsMessageAttribute.RecentPeer]) { diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index f6656ad29b1..afdc774ef56 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -391,33 +391,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI if (flags & (1 << 1)) != 0 { parsedFlags.insert(.shippingAddressRequested) } - - let extendedMedia: TelegramExtendedMedia? - if let apiExtendedMedia = apiExtendedMedia { - switch apiExtendedMedia { - case let .messageExtendedMediaPreview(_, width, height, thumb, videoDuration): - var dimensions: PixelDimensions? - if let width = width, let height = height { - dimensions = PixelDimensions(width: width, height: height) - } - var immediateThumbnailData: Data? - if let thumb = thumb, case let .photoStrippedSize(_, bytes) = thumb { - immediateThumbnailData = bytes.makeData() - } - extendedMedia = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration) - case let .messageExtendedMedia(apiMedia): - let (media, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId) - if let media = media { - extendedMedia = .full(media: media) - } else { - extendedMedia = nil - } - } - } else { - extendedMedia = nil - } - - return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: extendedMedia, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil, nil) + return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: apiExtendedMedia.flatMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) }), flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil, nil) case let .messageMediaPoll(poll, results): switch poll { case let .poll(id, flags, question, answers, closePeriod, _): @@ -464,6 +438,8 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI flags.insert(.refunded) } return (TelegramMediaGiveawayResults(flags: flags, launchMessageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: launchMsgId), additionalChannelsCount: additionalPeersCount ?? 0, winnersPeerIds: winners.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }, winnersCount: winnersCount, unclaimedCount: unclaimedCount, months: months, untilDate: untilDate, prizeDescription: prizeDescription), nil, nil, nil, nil) + case let .messageMediaPaidMedia(starsAmount, apiExtendedMedia): + return (TelegramMediaPaidContent(amount: starsAmount, extendedMedia: apiExtendedMedia.compactMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) })), nil, nil, nil, nil) } } @@ -473,8 +449,8 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { func coodinatesFromApiMediaAreaCoordinates(_ coordinates: Api.MediaAreaCoordinates) -> MediaArea.Coordinates { switch coordinates { - case let .mediaAreaCoordinates(x, y, width, height, rotation): - return MediaArea.Coordinates(x: x, y: y, width: width, height: height, rotation: rotation) + case let .mediaAreaCoordinates(_, x, y, width, height, rotation, radius): + return MediaArea.Coordinates(x: x, y: y, width: width, height: height, rotation: rotation, cornerRadius: radius) } } switch mediaArea { @@ -482,7 +458,7 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { return nil case .inputMediaAreaVenue: return nil - case let .mediaAreaGeoPoint(coordinates, geo): + case let .mediaAreaGeoPoint(_, coordinates, geo, address): let latitude: Double let longitude: Double switch geo { @@ -493,7 +469,28 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { latitude = 0.0 longitude = 0.0 } - return .venue(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), venue: MediaArea.Venue(latitude: latitude, longitude: longitude, venue: nil, queryId: nil, resultId: nil)) + + var mappedAddress: MapGeoAddress? + if let address { + switch address { + case let .geoPointAddress(_, countryIso2, state, city, street): + mappedAddress = MapGeoAddress( + country: countryIso2, + state: state, + city: city, + street: street + ) + } + } + + return .venue(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), venue: MediaArea.Venue( + latitude: latitude, + longitude: longitude, + venue: nil, + address: mappedAddress, + queryId: nil, + resultId: nil + )) case let .mediaAreaVenue(coordinates, geo, title, address, provider, venueId, venueType): let latitude: Double let longitude: Double @@ -505,7 +502,7 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { latitude = 0.0 longitude = 0.0 } - return .venue(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), venue: MediaArea.Venue(latitude: latitude, longitude: longitude, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: venueType), queryId: nil, resultId: nil)) + return .venue(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), venue: MediaArea.Venue(latitude: latitude, longitude: longitude, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: venueType), address: nil, queryId: nil, resultId: nil)) case let .mediaAreaSuggestedReaction(flags, coordinates, reaction): if let reaction = MessageReaction.Reaction(apiReaction: reaction) { var parsedFlags = MediaArea.ReactionFlags() @@ -519,16 +516,22 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { } else { return nil } + case let .mediaAreaUrl(coordinates, url): + return .link(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), url: url) case let .mediaAreaChannelPost(coordinates, channelId, messageId): return .channelMessage(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), messageId: EngineMessage.Id(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId)) } } -func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea], transaction: Transaction) -> [Api.MediaArea] { +func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea], transaction: Transaction?) -> [Api.MediaArea] { var apiMediaAreas: [Api.MediaArea] = [] for area in mediaAreas { let coordinates = area.coordinates - let inputCoordinates = Api.MediaAreaCoordinates.mediaAreaCoordinates(x: coordinates.x, y: coordinates.y, w: coordinates.width, h: coordinates.height, rotation: coordinates.rotation) + var flags: Int32 = 0 + if let _ = coordinates.cornerRadius { + flags |= (1 << 0) + } + let inputCoordinates = Api.MediaAreaCoordinates.mediaAreaCoordinates(flags: flags, x: coordinates.x, y: coordinates.y, w: coordinates.width, h: coordinates.height, rotation: coordinates.rotation, radius: coordinates.cornerRadius) switch area { case let .venue(_, venue): if let queryId = venue.queryId, let resultId = venue.resultId { @@ -536,7 +539,23 @@ func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea], transaction: Transac } else if let venueInfo = venue.venue { apiMediaAreas.append(.mediaAreaVenue(coordinates: inputCoordinates, geo: .geoPoint(flags: 0, long: venue.longitude, lat: venue.latitude, accessHash: 0, accuracyRadius: nil), title: venueInfo.title, address: venueInfo.address ?? "", provider: venueInfo.provider ?? "", venueId: venueInfo.id ?? "", venueType: venueInfo.type ?? "")) } else { - apiMediaAreas.append(.mediaAreaGeoPoint(coordinates: inputCoordinates, geo: .geoPoint(flags: 0, long: venue.longitude, lat: venue.latitude, accessHash: 0, accuracyRadius: nil))) + var flags: Int32 = 0 + var inputAddress: Api.GeoPointAddress? + if let address = venue.address { + var addressFlags: Int32 = 0 + if let _ = address.state { + addressFlags |= (1 << 0) + } + if let _ = address.city { + addressFlags |= (1 << 1) + } + if let _ = address.street { + addressFlags |= (1 << 2) + } + inputAddress = .geoPointAddress(flags: addressFlags, countryIso2: address.country, state: address.state, city: address.city, street: address.street) + flags |= (1 << 0) + } + apiMediaAreas.append(.mediaAreaGeoPoint(flags: flags, coordinates: inputCoordinates, geo: .geoPoint(flags: 0, long: venue.longitude, lat: venue.latitude, accessHash: 0, accuracyRadius: nil), address: inputAddress)) } case let .reaction(_, reaction, flags): var apiFlags: Int32 = 0 @@ -548,9 +567,11 @@ func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea], transaction: Transac } apiMediaAreas.append(.mediaAreaSuggestedReaction(flags: apiFlags, coordinates: inputCoordinates, reaction: reaction.apiReaction)) case let .channelMessage(_, messageId): - if let peer = transaction.getPeer(messageId.peerId), let inputChannel = apiInputChannel(peer) { + if let transaction, let peer = transaction.getPeer(messageId.peerId), let inputChannel = apiInputChannel(peer) { apiMediaAreas.append(.inputMediaAreaChannelPost(coordinates: inputCoordinates, channel: inputChannel, msgId: messageId.id)) } + case let .link(_, url): + apiMediaAreas.append(.mediaAreaUrl(coordinates: inputCoordinates, url: url)) } } return apiMediaAreas diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramExtendedMedia.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramExtendedMedia.swift new file mode 100644 index 00000000000..e82718a39bf --- /dev/null +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramExtendedMedia.swift @@ -0,0 +1,27 @@ +import Foundation +import Postbox +import TelegramApi + +extension TelegramExtendedMedia { + init?(apiExtendedMedia: Api.MessageExtendedMedia, peerId: PeerId) { + switch apiExtendedMedia { + case let .messageExtendedMediaPreview(_, width, height, thumb, videoDuration): + var dimensions: PixelDimensions? + if let width = width, let height = height { + dimensions = PixelDimensions(width: width, height: height) + } + var immediateThumbnailData: Data? + if let thumb = thumb, case let .photoStrippedSize(_, bytes) = thumb { + immediateThumbnailData = bytes.makeData() + } + self = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration) + case let .messageExtendedMedia(apiMedia): + let (media, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId) + if let media = media { + self = .full(media: media) + } else { + return nil + } + } + } +} diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaMap.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaMap.swift index f0e9de92ef8..84d935b8a5f 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaMap.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaMap.swift @@ -10,8 +10,16 @@ func telegramMediaMapFromApiGeoPoint(_ geo: Api.GeoPoint, title: String?, addres } switch geo { case let .geoPoint(_, long, lat, _, accuracyRadius): - return TelegramMediaMap(latitude: lat, longitude: long, heading: heading, accuracyRadius: accuracyRadius.flatMap { Double($0) }, geoPlace: nil, venue: venue, liveBroadcastingTimeout: liveBroadcastingTimeout, liveProximityNotificationRadius: liveProximityNotificationRadius) + return TelegramMediaMap(latitude: lat, longitude: long, heading: heading, accuracyRadius: accuracyRadius.flatMap { Double($0) }, venue: venue, liveBroadcastingTimeout: liveBroadcastingTimeout, liveProximityNotificationRadius: liveProximityNotificationRadius) case .geoPointEmpty: - return TelegramMediaMap(latitude: 0.0, longitude: 0.0, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: venue, liveBroadcastingTimeout: liveBroadcastingTimeout, liveProximityNotificationRadius: liveProximityNotificationRadius) + return TelegramMediaMap(latitude: 0.0, longitude: 0.0, heading: nil, accuracyRadius: nil, venue: venue, liveBroadcastingTimeout: liveBroadcastingTimeout, liveProximityNotificationRadius: liveProximityNotificationRadius) + } +} + + +func mapGeoAddressFromApiGeoPointAddress(_ geo: Api.GeoPointAddress) -> MapGeoAddress { + switch geo { + case let .geoPointAddress(_, countryIso2, state, city, street): + return MapGeoAddress(country: countryIso2, state: state, city: city, street: street) } } diff --git a/submodules/TelegramCore/Sources/Authorization.swift b/submodules/TelegramCore/Sources/Authorization.swift index f7e5bd5e3b5..6873cbef1d0 100644 --- a/submodules/TelegramCore/Sources/Authorization.swift +++ b/submodules/TelegramCore/Sources/Authorization.swift @@ -289,7 +289,7 @@ public func sendAuthorizationCode(accountManager: AccountManager map { mapping -> String? in guard let receipt = receipt else { @@ -380,7 +380,7 @@ public func sendAuthorizationCode(accountManager: AccountManager map { mapping -> String? in guard let receipt = receipt else { @@ -589,7 +589,7 @@ public func resendAuthorizationCode(accountManager: AccountManager map { mapping -> String? in guard let receipt = receipt else { diff --git a/submodules/TelegramCore/Sources/Network/FetchV2.swift b/submodules/TelegramCore/Sources/Network/FetchV2.swift index b454a2a770c..6d460bf17bb 100644 --- a/submodules/TelegramCore/Sources/Network/FetchV2.swift +++ b/submodules/TelegramCore/Sources/Network/FetchV2.swift @@ -73,6 +73,44 @@ private final class FetchImpl { } } + private final class PendingReadyPart { + let partRange: Range + let fetchRange: Range + let fetchedData: Data + let decryptedData: Data + + init( + partRange: Range, + fetchRange: Range, + fetchedData: Data, + decryptedData: Data + ) { + self.partRange = partRange + self.fetchRange = fetchRange + self.fetchedData = fetchedData + self.decryptedData = decryptedData + } + } + + private final class PendingHashRange { + let range: Range + var disposable: Disposable? + + init(range: Range) { + self.range = range + } + } + + private final class HashRangeData { + let range: Range + let data: Data + + init(range: Range, data: Data) { + self.range = range + self.data = data + } + } + private final class CdnData { let id: Int let sourceDatacenterId: Int @@ -95,6 +133,16 @@ private final class FetchImpl { } } + private final class VerifyPartHashData { + let fetchRange: Range + let fetchedData: Data + + init(fetchRange: Range, fetchedData: Data) { + self.fetchRange = fetchRange + self.fetchedData = fetchedData + } + } + private enum FetchLocation { case datacenter(Int) case cdn(CdnData) @@ -111,6 +159,12 @@ private final class FetchImpl { var pendingParts: [PendingPart] = [] var completedRanges = RangeSet() + + var pendingReadyParts: [PendingReadyPart] = [] + var completedHashRanges = RangeSet() + var pendingHashRanges: [PendingHashRange] = [] + var hashRanges: [Int64: HashRangeData] = [:] + var nextRangePriorityIndex: Int = 0 init( @@ -132,8 +186,11 @@ private final class FetchImpl { } deinit { - for peindingPart in self.pendingParts { - peindingPart.disposable?.dispose() + for pendingPart in self.pendingParts { + pendingPart.disposable?.dispose() + } + for pendingHashRange in self.pendingHashRanges { + pendingHashRange.disposable?.dispose() } } } @@ -216,6 +273,7 @@ private final class FetchImpl { private var requiredRanges: [RequiredRange] = [] private let defaultPartSize: Int64 + private let cdnPartSize: Int64 private var state: State? private let loggingIdentifier: String @@ -281,6 +339,7 @@ private final class FetchImpl { } else { self.defaultPartSize = 128 * 1024 } + self.cdnPartSize = 128 * 1024 if let resource = resource as? TelegramCloudMediaResource { if let apiInputLocation = resource.apiInputLocation(fileReference: Data()) { @@ -335,6 +394,82 @@ private final class FetchImpl { self.onNext(.resourceSizeUpdated(knownSize)) } + do { + var removedPendingReadyPartIndices: [Int] = [] + for i in 0 ..< state.pendingReadyParts.count { + let pendingReadyPart = state.pendingReadyParts[i] + if state.completedHashRanges.isSuperset(of: RangeSet(pendingReadyPart.fetchRange)) { + removedPendingReadyPartIndices.append(i) + + var checkOffset: Int64 = 0 + var checkFailed = false + while checkOffset < pendingReadyPart.fetchedData.count { + if let hashRange = state.hashRanges[pendingReadyPart.fetchRange.lowerBound + checkOffset] { + var clippedHashRange = hashRange.range + + if pendingReadyPart.fetchRange.lowerBound + Int64(pendingReadyPart.fetchedData.count) < clippedHashRange.lowerBound { + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): unable to check \(pendingReadyPart.fetchRange): data range \(clippedHashRange) out of bounds (0 ..< \(pendingReadyPart.fetchedData.count))") + checkFailed = true + break + } + clippedHashRange = clippedHashRange.lowerBound ..< min(clippedHashRange.upperBound, pendingReadyPart.fetchRange.lowerBound + Int64(pendingReadyPart.fetchedData.count)) + + let partLocalHashRange = (clippedHashRange.lowerBound - pendingReadyPart.fetchRange.lowerBound) ..< (clippedHashRange.upperBound - pendingReadyPart.fetchRange.lowerBound) + + if partLocalHashRange.lowerBound < 0 || partLocalHashRange.upperBound > pendingReadyPart.fetchedData.count { + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): unable to check \(pendingReadyPart.fetchRange): data range \(partLocalHashRange) out of bounds (0 ..< \(pendingReadyPart.fetchedData.count))") + checkFailed = true + break + } + + let dataToHash = pendingReadyPart.decryptedData.subdata(in: Int(partLocalHashRange.lowerBound) ..< Int(partLocalHashRange.upperBound)) + let localHash = MTSha256(dataToHash) + if localHash != hashRange.data { + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): failed to verify \(pendingReadyPart.fetchRange): hash mismatch") + checkFailed = true + break + } + + checkOffset += partLocalHashRange.upperBound - partLocalHashRange.lowerBound + } else { + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): unable to find \(pendingReadyPart.fetchRange) hash range despite it being marked as ready") + checkFailed = true + break + } + } + if !checkFailed { + self.commitPendingReadyPart(state: state, partRange: pendingReadyPart.partRange, fetchRange: pendingReadyPart.fetchRange, data: pendingReadyPart.decryptedData) + } else { + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): unable to find \(pendingReadyPart.fetchRange) hash check failed") + } + } + } + for index in removedPendingReadyPartIndices.reversed() { + state.pendingReadyParts.remove(at: index) + } + } + + var requiredHashRanges = RangeSet() + for pendingReadyPart in state.pendingReadyParts { + //TODO:check if already have hashes + requiredHashRanges.formUnion(RangeSet(pendingReadyPart.fetchRange)) + } + requiredHashRanges.subtract(state.completedHashRanges) + for pendingHashRange in state.pendingHashRanges { + requiredHashRanges.subtract(RangeSet(pendingHashRange.range)) + } + + let expectedHashRangeLength: Int64 = 1 * 1024 * 1024 + while state.pendingHashRanges.count < state.maxPendingParts { + guard let requiredHashRange = requiredHashRanges.ranges.first else { + break + } + let hashRange: Range = requiredHashRange.lowerBound ..< (requiredHashRange.lowerBound + expectedHashRangeLength) + requiredHashRanges.subtract(RangeSet(hashRange)) + + state.pendingHashRanges.append(FetchImpl.PendingHashRange(range: hashRange)) + } + var filteredRequiredRanges: [RangeSet] = [] for _ in 0 ..< 3 { filteredRequiredRanges.append(RangeSet()) @@ -355,28 +490,14 @@ private final class FetchImpl { for pendingPart in state.pendingParts { filteredRequiredRanges[i].remove(contentsOf: pendingPart.partRange) } + for pendingReadyPart in state.pendingReadyParts { + filteredRequiredRanges[i].remove(contentsOf: pendingReadyPart.partRange) + } excludedInHigherPriorities.subtract(filteredRequiredRanges[i]) } - /*for _ in 0 ..< 1000000 { - let i = Int64.random(in: 0 ..< 1024 * 1024 + 500 * 1024) - let j = Int64.random(in: 1 ... state.partSize) - - let firstRange: Range = Int64(i) ..< (Int64(i) + j) - - let partRange = firstRange.lowerBound ..< min(firstRange.upperBound, firstRange.lowerBound + state.partSize) - - let _ = alignPartFetchRange( - partRange: partRange, - minPartSize: state.minPartSize, - maxPartSize: state.maxPartSize, - alignment: state.partAlignment, - boundaryLimit: state.partDivision - ) - }*/ - - if state.pendingParts.count < state.maxPendingParts { + if state.pendingParts.count < state.maxPendingParts && state.pendingReadyParts.count < state.maxPendingParts { var debugRangesString = "" for priorityIndex in 0 ..< 3 { if filteredRequiredRanges[priorityIndex].isEmpty { @@ -404,7 +525,7 @@ private final class FetchImpl { Logger.shared.log("FetchV2", "\(self.loggingIdentifier): will fetch \(debugRangesString)") } - while state.pendingParts.count < state.maxPendingParts { + while state.pendingParts.count < state.maxPendingParts && state.pendingReadyParts.count < state.maxPendingParts { var found = false inner: for i in 0 ..< filteredRequiredRanges.count { let priorityIndex = (state.nextRangePriorityIndex + i) % filteredRequiredRanges.count @@ -423,14 +544,24 @@ private final class FetchImpl { boundaryLimit: state.partDivision ) - Logger.shared.log("FetchV2", "\(self.loggingIdentifier): take part \(partRange) (aligned as \(alignedRange))") + var storePartRange = partRange + do { + storePartRange = alignedRange + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): take part \(partRange) (store aligned as \(storePartRange)") + } + /*if case .cdn = state.fetchLocation { + storePartRange = alignedRange + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): take part \(partRange) (store aligned as \(storePartRange)") + } else { + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): take part \(partRange) (aligned as \(alignedRange))") + }*/ let pendingPart = PendingPart( - partRange: partRange, + partRange: storePartRange, fetchRange: alignedRange ) state.pendingParts.append(pendingPart) - filteredRequiredRanges[priorityIndex].remove(contentsOf: partRange) + filteredRequiredRanges[priorityIndex].remove(contentsOf: storePartRange) found = true break inner @@ -446,6 +577,11 @@ private final class FetchImpl { self.fetchPart(state: state, part: pendingPart) } } + for pendingHashRange in state.pendingHashRanges { + if pendingHashRange.disposable == nil { + self.fetchHashRange(state: state, hashRange: pendingHashRange) + } + } case let .reuploadingToCdn(state): if state.disposable == nil { Logger.shared.log("FetchV2", "\(self.loggingIdentifier): refreshing CDN") @@ -472,10 +608,10 @@ private final class FetchImpl { } self.state = .fetching(FetchImpl.FetchingState( fetchLocation: .cdn(cdnData), - partSize: self.defaultPartSize, - minPartSize: 4 * 1024, - maxPartSize: self.defaultPartSize, - partAlignment: 4 * 1024, + partSize: self.cdnPartSize, + minPartSize: self.cdnPartSize, + maxPartSize: self.cdnPartSize * 2, + partAlignment: self.cdnPartSize, partDivision: 1 * 1024 * 1024, maxPendingParts: 6 )) @@ -549,7 +685,7 @@ private final class FetchImpl { } enum FilePartResult { - case data(Data) + case data(data: Data, verifyPartHashData: VerifyPartHashData?) case cdnRedirect(CdnData) case cdnRefresh(cdnData: CdnData, refreshToken: Data) case fileReferenceExpired @@ -564,6 +700,7 @@ private final class FetchImpl { switch state.fetchLocation { case let .cdn(cdnData): let requestedOffset = part.fetchRange.lowerBound + filePartRequest = self.network.multiplexedRequestManager.request( to: .cdn(cdnData.id), consumerId: self.consumerId, @@ -581,7 +718,7 @@ private final class FetchImpl { switch result { case let .cdnFile(bytes): if bytes.size == 0 { - return .data(Data()) + return .data(data: Data(), verifyPartHashData: nil) } else { var partIv = cdnData.encryptionIv let partIvCount = partIv.count @@ -590,8 +727,12 @@ private final class FetchImpl { var ivOffset: Int32 = Int32(clamping: (requestedOffset / 16)).bigEndian memcpy(bytes.advanced(by: partIvCount - 4), &ivOffset, 4) } - //TODO:check hashes - return .data(MTAesCtrDecrypt(bytes.makeData(), cdnData.encryptionKey, partIv)!) + + let fetchedData = bytes.makeData() + return .data( + data: MTAesCtrDecrypt(fetchedData, cdnData.encryptionKey, partIv)!, + verifyPartHashData: VerifyPartHashData(fetchRange: fetchRange, fetchedData: fetchedData) + ) } case let .cdnFileReuploadNeeded(requestToken): return .cdnRefresh(cdnData: cdnData, refreshToken: requestToken.makeData()) @@ -609,6 +750,7 @@ private final class FetchImpl { fileReference = info.reference.apiFileReference } if let inputLocation = cloudResource.apiInputLocation(fileReference: fileReference) { + let queue = self.queue filePartRequest = self.network.multiplexedRequestManager.request( to: .main(sourceDatacenterId), consumerId: self.consumerId, @@ -620,12 +762,19 @@ private final class FetchImpl { limit: Int32(requestedLength)), tag: self.parameters?.tag, continueInBackground: self.continueInBackground, - expectedResponseSize: Int32(requestedLength) + onFloodWaitError: { [weak self] error in + queue.async { + guard let self else { + return + } + self.processFloodWaitError(error: error) + } + }, expectedResponseSize: Int32(requestedLength) ) |> map { result -> FilePartResult in switch result { case let .file(_, _, bytes): - return .data(bytes.makeData()) + return .data(data: bytes.makeData(), verifyPartHashData: nil) case let .fileCdnRedirect(dcId, fileToken, encryptionKey, encryptionIv, fileHashes): let _ = fileHashes return .cdnRedirect(CdnData( @@ -648,73 +797,45 @@ private final class FetchImpl { } } - if let filePartRequest = filePartRequest { + if let filePartRequest { part.disposable = (filePartRequest |> deliverOn(self.queue)).start(next: { [weak self, weak state, weak part] result in - guard let `self` = self, let state = state, case let .fetching(fetchingState) = self.state, fetchingState === state else { + guard let self, let state, case let .fetching(fetchingState) = self.state, fetchingState === state else { return } - if let part = part { + if let part { if let index = state.pendingParts.firstIndex(where: { $0 === part }) { state.pendingParts.remove(at: index) } } switch result { - case let .data(data): - let actualLength = Int64(data.count) - - if actualLength < requestedLength { - let resultingSize = fetchRange.lowerBound + actualLength - if let currentKnownSize = self.knownSize { - Logger.shared.log("FetchV2", "\(self.loggingIdentifier): setting known size to min(\(currentKnownSize), \(resultingSize)) = \(min(currentKnownSize, resultingSize))") - self.knownSize = min(currentKnownSize, resultingSize) - } else { - Logger.shared.log("FetchV2", "\(self.loggingIdentifier): setting known size to \(resultingSize)") - self.knownSize = resultingSize - } - Logger.shared.log("FetchV2", "\(self.loggingIdentifier): reporting resource size \(resultingSize)") - self.onNext(.resourceSizeUpdated(resultingSize)) - } - - state.completedRanges.formUnion(RangeSet(partRange)) - - var actualData = data - if partRange != fetchRange { - precondition(partRange.lowerBound >= fetchRange.lowerBound) - precondition(partRange.upperBound <= fetchRange.upperBound) - let innerOffset = partRange.lowerBound - fetchRange.lowerBound - var innerLength = partRange.upperBound - partRange.lowerBound - innerLength = min(innerLength, Int64(actualData.count - Int(innerOffset))) - if innerLength > 0 { - actualData = actualData.subdata(in: Int(innerOffset) ..< Int(innerOffset + innerLength)) - } else { - actualData = Data() - } - - Logger.shared.log("FetchV2", "\(self.loggingIdentifier): extracting aligned part \(partRange) (\(fetchRange)): \(actualData.count)") - } - - if !actualData.isEmpty { - Logger.shared.log("FetchV2", "\(self.loggingIdentifier): emitting data part \(partRange) (aligned as \(fetchRange)): \(actualData.count)") + case let .data(data, verifyPartHashData): + if let verifyPartHashData { + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): stashing data part \(partRange) (aligned as \(fetchRange)) for hash verification") - self.onNext(.dataPart( - resourceOffset: partRange.lowerBound, - data: actualData, - range: 0 ..< Int64(actualData.count), - complete: false + state.pendingReadyParts.append(FetchImpl.PendingReadyPart( + partRange: partRange, + fetchRange: fetchRange, + fetchedData: verifyPartHashData.fetchedData, + decryptedData: data )) } else { - Logger.shared.log("FetchV2", "\(self.loggingIdentifier): not emitting data part \(partRange) (aligned as \(fetchRange))") + self.commitPendingReadyPart( + state: state, + partRange: partRange, + fetchRange: fetchRange, + data: data + ) } case let .cdnRedirect(cdnData): self.state = .fetching(FetchImpl.FetchingState( fetchLocation: .cdn(cdnData), - partSize: self.defaultPartSize, - minPartSize: 4 * 1024, - maxPartSize: self.defaultPartSize, - partAlignment: 4 * 1024, + partSize: self.cdnPartSize, + minPartSize: self.cdnPartSize, + maxPartSize: self.cdnPartSize * 2, + partAlignment: self.cdnPartSize, partDivision: 1 * 1024 * 1024, maxPendingParts: 6 )) @@ -735,6 +856,129 @@ private final class FetchImpl { //assertionFailure() } } + + private func fetchHashRange(state: FetchingState, hashRange: PendingHashRange) { + let fetchRequest: Signal<[Api.FileHash]?, NoError> + + switch state.fetchLocation { + case let .cdn(cdnData): + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): will fetch hashes for \(hashRange.range)") + + fetchRequest = self.network.multiplexedRequestManager.request( + to: .main(cdnData.sourceDatacenterId), + consumerId: self.consumerId, + resourceId: self.resource.id.stringRepresentation, + data: Api.functions.upload.getCdnFileHashes(fileToken: Buffer(data: cdnData.fileToken), offset: hashRange.range.lowerBound), + tag: self.parameters?.tag, + continueInBackground: self.continueInBackground, + expectedResponseSize: nil + ) + |> map(Optional.init) + |> `catch` { _ -> Signal<[Api.FileHash]?, NoError> in + return .single(nil) + } + case .datacenter: + fetchRequest = .single(nil) + } + + let queue = self.queue + hashRange.disposable = (fetchRequest + |> deliverOn(self.queue)).start(next: { [weak self, weak state, weak hashRange] result in + queue.async { + guard let self, let state, case let .fetching(fetchingState) = self.state, fetchingState === state else { + return + } + + if let result { + if let hashRange { + if let index = state.pendingHashRanges.firstIndex(where: { $0 === hashRange }) { + state.pendingHashRanges.remove(at: index) + } + } + + var filledRange = RangeSet() + for hashItem in result { + switch hashItem { + case let .fileHash(offset, limit, hash): + let rangeValue: Range = offset ..< (offset + Int64(limit)) + filledRange.formUnion(RangeSet(rangeValue)) + state.hashRanges[rangeValue.lowerBound] = HashRangeData( + range: rangeValue, + data: hash.makeData() + ) + state.completedHashRanges.formUnion(RangeSet(rangeValue)) + } + } + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): received hashes for \(filledRange)") + } + + self.update() + } + }) + } + + private func commitPendingReadyPart(state: FetchingState, partRange: Range, fetchRange: Range, data: Data) { + let requestedLength = fetchRange.upperBound - fetchRange.lowerBound + let actualLength = Int64(data.count) + + if actualLength < requestedLength { + let resultingSize = fetchRange.lowerBound + actualLength + if let currentKnownSize = self.knownSize { + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): setting known size to min(\(currentKnownSize), \(resultingSize)) = \(min(currentKnownSize, resultingSize))") + self.knownSize = min(currentKnownSize, resultingSize) + } else { + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): setting known size to \(resultingSize)") + self.knownSize = resultingSize + } + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): reporting resource size \(resultingSize)") + self.onNext(.resourceSizeUpdated(resultingSize)) + } + + state.completedRanges.formUnion(RangeSet(partRange)) + + var actualData = data + if partRange != fetchRange { + precondition(partRange.lowerBound >= fetchRange.lowerBound) + precondition(partRange.upperBound <= fetchRange.upperBound) + let innerOffset = partRange.lowerBound - fetchRange.lowerBound + var innerLength = partRange.upperBound - partRange.lowerBound + innerLength = min(innerLength, Int64(actualData.count - Int(innerOffset))) + if innerLength > 0 { + actualData = actualData.subdata(in: Int(innerOffset) ..< Int(innerOffset + innerLength)) + } else { + actualData = Data() + } + + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): extracting aligned part \(partRange) (\(fetchRange)): \(actualData.count)") + } + + if !actualData.isEmpty { + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): emitting data part \(partRange) (aligned as \(fetchRange)): \(actualData.count)") + + self.onNext(.dataPart( + resourceOffset: partRange.lowerBound, + data: actualData, + range: 0 ..< Int64(actualData.count), + complete: false + )) + } else { + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): not emitting data part \(partRange) (aligned as \(fetchRange))") + } + } + + private func processFloodWaitError(error: String) { + var networkSpeedLimitSubject: NetworkSpeedLimitedEvent.DownloadSubject? + if let location = self.parameters?.location { + if let messageId = location.messageId { + networkSpeedLimitSubject = .message(messageId) + } + } + if let subject = networkSpeedLimitSubject { + if error.hasPrefix("FLOOD_PREMIUM_WAIT") { + self.network.addNetworkSpeedLimitedEvent(event: .download(subject)) + } + } + } } private static let sharedQueue = Queue(name: "FetchImpl") diff --git a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index fd2ee567768..00f94cf8840 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -153,7 +153,13 @@ private func areResourcesEqual(_ lhs: MediaResource, _ rhs: MediaResource) -> Bo } private func findMediaResource(media: Media, previousMedia: Media?, resource: MediaResource) -> TelegramMediaResource? { - if let image = media as? TelegramMediaImage { + if let paidContent = media as? TelegramMediaPaidContent { + for case let .full(fullMedia) in paidContent.extendedMedia { + if let resource = findMediaResource(media: fullMedia, previousMedia: previousMedia, resource: resource) { + return resource + } + } + } else if let image = media as? TelegramMediaImage { for representation in image.representations { if let updatedResource = representation.resource as? CloudPhotoSizeMediaResource, let previousResource = resource as? CloudPhotoSizeMediaResource { if updatedResource.photoId == previousResource.photoId && updatedResource.sizeSpec == previousResource.sizeSpec { diff --git a/submodules/TelegramCore/Sources/Network/Network.swift b/submodules/TelegramCore/Sources/Network/Network.swift index f7e2c038895..58214119182 100644 --- a/submodules/TelegramCore/Sources/Network/Network.swift +++ b/submodules/TelegramCore/Sources/Network/Network.swift @@ -630,7 +630,10 @@ func initializedNetwork(accountId: AccountRecordId, arguments: NetworkInitializa mtProto.delegate = connectionStatusDelegate mtProto.add(requestService) - let useExperimentalFeatures = networkSettings?.useExperimentalDownload ?? false + var useExperimentalFeatures = networkSettings?.useExperimentalDownload ?? true + if let data = appConfiguration.data, let _ = data["ios_killswitch_disable_downloadv2"] { + useExperimentalFeatures = false + } let network = Network(queue: queue, datacenterId: datacenterId, context: context, mtProto: mtProto, requestService: requestService, connectionStatusDelegate: connectionStatusDelegate, _connectionStatus: connectionStatus, basePath: basePath, appDataDisposable: appDataDisposable, encryptionProvider: arguments.encryptionProvider, useRequestTimeoutTimers: useRequestTimeoutTimers, useBetaFeatures: arguments.useBetaFeatures, useExperimentalFeatures: useExperimentalFeatures) @@ -926,7 +929,11 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate { datacenterId = id isCdn = true } - return strongSelf.makeWorker(datacenterId: datacenterId, isCdn: isCdn, isMedia: isMedia, tag: tag, continueInBackground: continueInBackground) + if datacenterId != 0 { + return strongSelf.makeWorker(datacenterId: datacenterId, isCdn: isCdn, isMedia: isMedia, tag: tag, continueInBackground: continueInBackground) + } else { + return nil + } } return nil }) diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index b2fa27871d4..f76bc6d1851 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -23,8 +23,18 @@ struct PendingMessageUploadedContentAndReuploadInfo { let cacheReferenceKey: CachedSentMediaReferenceKey? } +struct PendingMessageUploadedContentProgress { + let progress: Float + let mediaProgress: [MediaId: Float] + + init(progress: Float, mediaProgress: [MediaId: Float] = [:]) { + self.progress = progress + self.mediaProgress = mediaProgress + } +} + enum PendingMessageUploadedContentResult { - case progress(Float) + case progress(PendingMessageUploadedContentProgress) case content(PendingMessageUploadedContentAndReuploadInfo) } @@ -90,7 +100,7 @@ func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Po } else if let media = media.first as? TelegramMediaStory { return .signal(postbox.transaction { transaction -> PendingMessageUploadedContentResult in guard let inputPeer = transaction.getPeer(media.storyId.peerId).flatMap(apiInputPeer) else { - return .progress(0.0) + return .progress(PendingMessageUploadedContentProgress(progress: 0.0)) } return .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaStory(peer: inputPeer, id: media.storyId.id), ""), reuploadInfo: nil, cacheReferenceKey: nil)) } @@ -119,6 +129,53 @@ func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Po } func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, forceNoBigParts: Bool, peerId: PeerId, media: Media, text: String, autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?, autoclearMessageAttribute: AutoclearTimeoutMessageAttribute?, messageId: MessageId?, attributes: [MessageAttribute], mediaReference: AnyMediaReference?) -> Signal? { + if let paidContent = media as? TelegramMediaPaidContent { + var signals: [Signal] = [] + var mediaIds: [MediaId] = [] + let isGrouped = paidContent.extendedMedia.count > 1 + for case let .full(media) in paidContent.extendedMedia { + guard let id = media.id else { + continue + } + mediaIds.append(id) + if let image = media as? TelegramMediaImage { + signals.append(uploadedMediaImageContent(network: network, postbox: postbox, transformOutgoingMessageMedia: transformOutgoingMessageMedia, forceReupload: forceReupload, isGrouped: isGrouped, peerId: peerId, image: image, messageId: messageId, text: "", attributes: [], autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, auxiliaryMethods: auxiliaryMethods)) + } else if let file = media as? TelegramMediaFile { + signals.append(uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, isPaid: true, passFetchProgress: false, forceNoBigParts: false, peerId: peerId, messageId: messageId, text: "", attributes: [], autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, file: file)) + } + } + return combineLatest(signals) + |> map { results -> PendingMessageUploadedContentResult in + var currentProgress: Float = 0.0 + var media: [Api.InputMedia] = [] + var mediaProgress: [MediaId: Float] = [:] + for (mediaId, result) in zip(mediaIds, results) { + switch result { + case let .progress(progress): + currentProgress += progress.progress + mediaProgress[mediaId] = progress.progress + case let .content(content): + if case let .media(resultMedia, _) = content.content { + media.append(resultMedia) + mediaProgress[mediaId] = 1.0 + } + } + } + let normalizedProgress = currentProgress / Float(results.count) + if media.count == results.count { + return .content(PendingMessageUploadedContentAndReuploadInfo( + content: .media(.inputMediaPaidMedia( + starsAmount: paidContent.amount, + extendedMedia: media + ), text), + reuploadInfo: nil, + cacheReferenceKey: nil + )) + } else { + return .progress(PendingMessageUploadedContentProgress(progress: normalizedProgress, mediaProgress: mediaProgress)) + } + } + } if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { if peerId.namespace == Namespaces.Peer.SecretChat, let resource = largest.resource as? SecretFileMediaResource { return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .secretMedia(.inputEncryptedFile(id: resource.fileId, accessHash: resource.accessHash), resource.decryptedSize, resource.key), reuploadInfo: nil, cacheReferenceKey: nil))) @@ -138,7 +195,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post } } } - return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: true, isGrouped: isGrouped, passFetchProgress: false, forceNoBigParts: false, peerId: peerId, messageId: messageId, text: text, attributes: attributes, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, file: file) + return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: true, isGrouped: isGrouped, isPaid: false, passFetchProgress: false, forceNoBigParts: false, peerId: peerId, messageId: messageId, text: text, attributes: attributes, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, file: file) } else { if forceReupload { let finalMediaReference: Signal @@ -190,7 +247,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: emojiSearchQuery), text), reuploadInfo: nil, cacheReferenceKey: nil))) } } else { - return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, messageId: messageId, text: text, attributes: attributes, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, file: file) + return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, isPaid: false, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, messageId: messageId, text: text, attributes: attributes, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, file: file) } } else if let contact = media as? TelegramMediaContact { let input = Api.InputMedia.inputMediaContact(phoneNumber: contact.phoneNumber, firstName: contact.firstName, lastName: contact.lastName, vcard: contact.vCardData ?? "") @@ -426,7 +483,7 @@ if "".isEmpty { flags |= 1 << 1 } } - return .single(.progress(1.0)) + return .single(.progress(PendingMessageUploadedContentProgress(progress: 1.0))) |> then( .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaPhoto(flags: flags, id: .inputPhoto(id: id, accessHash: accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: ttlSeconds), text), reuploadInfo: nil, cacheReferenceKey: nil))) ) @@ -462,13 +519,43 @@ if "".isEmpty { storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: nil, psaType: nil, flags: []) } var updatedAttributes = currentMessage.attributes - if let index = updatedAttributes.firstIndex(where: { $0 is OutgoingMessageInfoAttribute }){ - let attribute = updatedAttributes[index] as! OutgoingMessageInfoAttribute - updatedAttributes[index] = attribute.withUpdatedFlags(attribute.flags.union([.transformedMedia])) - } else { - updatedAttributes.append(OutgoingMessageInfoAttribute(uniqueId: Int64.random(in: Int64.min ... Int64.max), flags: [.transformedMedia], acknowledged: false, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + + var markTransformedMedia = true + var updatedMedia = currentMessage.media + if let paidContent = updatedMedia.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent { + var extendedMedia = paidContent.extendedMedia + if let index = extendedMedia.firstIndex(where: { media in + if case let .full(fullMedia) = media, fullMedia.id == id { + return true + } else { + return false + } + }) { + extendedMedia[index] = .full(media: media) + } + updatedMedia = [TelegramMediaPaidContent(amount: paidContent.amount, extendedMedia: extendedMedia)] + + if extendedMedia.contains(where: { media in + if case .preview = media { + return true + } else { + return false + } + }) { + markTransformedMedia = false + } + } + + if markTransformedMedia { + if let index = updatedAttributes.firstIndex(where: { $0 is OutgoingMessageInfoAttribute }){ + let attribute = updatedAttributes[index] as! OutgoingMessageInfoAttribute + updatedAttributes[index] = attribute.withUpdatedFlags(attribute.flags.union([.transformedMedia])) + } else { + updatedAttributes.append(OutgoingMessageInfoAttribute(uniqueId: Int64.random(in: Int64.min ... Int64.max), flags: [.transformedMedia], acknowledged: false, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + } } - return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: currentMessage.media)) + + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: updatedMedia)) }) } return .done(media) @@ -488,7 +575,7 @@ if "".isEmpty { |> mapToSignal { transformResult -> Signal in switch transformResult { case .pending: - return .single(.progress(0.0)) + return .single(.progress(PendingMessageUploadedContentProgress(progress: 0.0))) case let .done(transformedMedia): let transformedImage = (transformedMedia as? TelegramMediaImage) ?? image guard let largestRepresentation = largestImageRepresentation(transformedImage.representations) else { @@ -505,7 +592,7 @@ if "".isEmpty { |> mapToSignal { next -> Signal in switch next { case let .progress(progress): - return .single(.progress(progress)) + return .single(.progress(PendingMessageUploadedContentProgress(progress: progress))) case let .inputFile(file): var flags: Int32 = 0 var ttlSeconds: Int32? @@ -712,7 +799,7 @@ public func statsCategoryForFileWithAttributes(_ attributes: [TelegramMediaFileA return .file } -private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, forceNoBigParts: Bool, peerId: PeerId, messageId: MessageId?, text: String, attributes: [MessageAttribute], autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?, autoclearMessageAttribute: AutoclearTimeoutMessageAttribute?, file: TelegramMediaFile) -> Signal { +private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, forceReupload: Bool, isGrouped: Bool, isPaid: Bool, passFetchProgress: Bool, forceNoBigParts: Bool, peerId: PeerId, messageId: MessageId?, text: String, attributes: [MessageAttribute], autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?, autoclearMessageAttribute: AutoclearTimeoutMessageAttribute?, file: TelegramMediaFile) -> Signal { return maybePredownloadedFileResource(postbox: postbox, auxiliaryMethods: auxiliaryMethods, peerId: peerId, resource: file.resource, autoRemove: autoremoveMessageAttribute != nil || autoclearMessageAttribute != nil, forceRefresh: forceReupload) |> mapToSignal { result -> Signal in var referenceKey: CachedSentMediaReferenceKey? @@ -732,7 +819,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili } } - return .single(.progress(1.0)) + return .single(.progress(PendingMessageUploadedContentProgress(progress: 1.0))) |> then( .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: ttlSeconds, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil))) ) @@ -806,13 +893,43 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: nil, psaType: nil, flags: []) } var updatedAttributes = currentMessage.attributes - if let index = updatedAttributes.firstIndex(where: { $0 is OutgoingMessageInfoAttribute }){ - let attribute = updatedAttributes[index] as! OutgoingMessageInfoAttribute - updatedAttributes[index] = attribute.withUpdatedFlags(attribute.flags.union([.transformedMedia])) - } else { - updatedAttributes.append(OutgoingMessageInfoAttribute(uniqueId: Int64.random(in: Int64.min ... Int64.max), flags: [.transformedMedia], acknowledged: false, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + + var markTransformedMedia = true + var updatedMedia = currentMessage.media + if let paidContent = updatedMedia.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent { + var extendedMedia = paidContent.extendedMedia + if let index = extendedMedia.firstIndex(where: { media in + if case let .full(fullMedia) = media, fullMedia.id == id { + return true + } else { + return false + } + }) { + extendedMedia[index] = .full(media: media) + } + updatedMedia = [TelegramMediaPaidContent(amount: paidContent.amount, extendedMedia: extendedMedia)] + + if extendedMedia.contains(where: { media in + if case .preview = media { + return true + } else { + return false + } + }) { + markTransformedMedia = false + } } - return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: currentMessage.media)) + + if markTransformedMedia { + if let index = updatedAttributes.firstIndex(where: { $0 is OutgoingMessageInfoAttribute }){ + let attribute = updatedAttributes[index] as! OutgoingMessageInfoAttribute + updatedAttributes[index] = attribute.withUpdatedFlags(attribute.flags.union([.transformedMedia])) + } else { + updatedAttributes.append(OutgoingMessageInfoAttribute(uniqueId: Int64.random(in: Int64.min ... Int64.max), flags: [.transformedMedia], acknowledged: false, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + } + } + + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: updatedMedia)) }) } return .done(media) @@ -863,7 +980,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili |> mapToSignal { content, fileAndThumbnailResult, resourceStatus -> Signal in guard let content = content else { if let resourceStatus = resourceStatus, case let .Fetching(_, progress) = resourceStatus { - return .single(.progress(progress * 0.33)) + return .single(.progress(PendingMessageUploadedContentProgress(progress: progress * 0.33))) } return .complete() } @@ -873,7 +990,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili if passFetchProgress { progress = 0.33 + progress * 0.67 } - return .single(.progress(progress)) + return .single(.progress(PendingMessageUploadedContentProgress(progress: progress))) case let .inputFile(inputFile): if case let .done(file, thumbnail) = fileAndThumbnailResult { var flags: Int32 = 0 @@ -899,7 +1016,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili } } - if !file.isAnimated { + if !file.isAnimated || isPaid { flags |= 1 << 3 } diff --git a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift index c298e8aac5b..1a131673d1b 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift @@ -54,7 +54,7 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, let uploadedMedia: Signal switch media { case .keep: - uploadedMedia = .single(.progress(0.0)) + uploadedMedia = .single(.progress(PendingMessageUploadedContentProgress(progress: 0.0))) |> then(.single(nil)) case let .update(media): let generateUploadSignal: (Bool) -> Signal? = { forceReupload in @@ -69,14 +69,14 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, forceNoBigParts: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: attributes, mediaReference: nil) } if let uploadSignal = generateUploadSignal(forceReupload) { - uploadedMedia = .single(.progress(0.027)) + uploadedMedia = .single(.progress(PendingMessageUploadedContentProgress(progress: 0.027))) |> then(uploadSignal) |> map { result -> PendingMessageUploadedContentResult? in switch result { - case let .progress(value): - return .progress(max(value, 0.027)) - case let .content(content): - return .content(content) + case let .progress(value): + return .progress(PendingMessageUploadedContentProgress(progress: max(value.progress, 0.027))) + case let .content(content): + return .content(content) } } |> `catch` { _ -> Signal in @@ -92,10 +92,10 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, var pendingMediaContent: PendingMessageUploadedContent? if let uploadedMediaResult = uploadedMediaResult { switch uploadedMediaResult { - case let .progress(value): - return .single(.progress(value)) - case let .content(content): - pendingMediaContent = content.content + case let .progress(value): + return .single(.progress(value.progress)) + case let .content(content): + pendingMediaContent = content.content } } return postbox.transaction { transaction -> (Peer?, Message?, SimpleDictionary) in @@ -236,7 +236,13 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, } else { updatedFlags.remove(.Incoming) } - return .update(message.withUpdatedLocalTags(updatedLocalTags).withUpdatedFlags(updatedFlags)) + + var updatedMedia = message.media + if let previousPaidContent = previousMessage.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent, case .full = previousPaidContent.extendedMedia.first { + updatedMedia = previousMessage.media + } + + return .update(message.withUpdatedLocalTags(updatedLocalTags).withUpdatedFlags(updatedFlags).withUpdatedMedia(updatedMedia)) }) } default: diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift index 7046de43f7c..9a0baa37cfd 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift @@ -206,7 +206,7 @@ public func standaloneSendEnqueueMessages( switch result.result { case let .progress(value): allDone = false - progressSum += value + progressSum += value.progress case let .content(content): allResults.append((content, result.media)) } diff --git a/submodules/TelegramCore/Sources/State/AccountState.swift b/submodules/TelegramCore/Sources/State/AccountState.swift index 554adf98454..844ad827df3 100644 --- a/submodules/TelegramCore/Sources/State/AccountState.swift +++ b/submodules/TelegramCore/Sources/State/AccountState.swift @@ -36,7 +36,7 @@ extension SentAuthorizationCodeType { self = .emailSetupRequired(appleSignInAllowed: (flags & (1 << 0)) != 0) case let .sentCodeTypeFragmentSms(url, length): self = .fragment(url: url, length: length) - case let .sentCodeTypeFirebaseSms(_, _, _, _, pushTimeout, length): + case let .sentCodeTypeFirebaseSms(_, _, _, _, _, pushTimeout, length): self = .firebase(pushTimeout: pushTimeout, length: length) case let .sentCodeTypeSmsWord(_, beginning): self = .word(startsWith: beginning) diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 49b340a520f..5e00964182a 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1554,7 +1554,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: switch draft { case .draftMessageEmpty: inputState = nil - case let .draftMessage(_, replyToMsgHeader, message, entities, media, date): + case let .draftMessage(_, replyToMsgHeader, message, entities, media, date, messageEffectId): let _ = media var replySubject: EngineMessageReplySubject? if let replyToMsgHeader = replyToMsgHeader { @@ -1600,7 +1600,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: break } } - inputState = SynchronizeableChatInputState(replySubject: replySubject, text: message, entities: messageTextEntitiesFromApiEntities(entities ?? []), timestamp: date, textSelection: nil) + inputState = SynchronizeableChatInputState(replySubject: replySubject, text: message, entities: messageTextEntitiesFromApiEntities(entities ?? []), timestamp: date, textSelection: nil, messageEffectId: messageEffectId) } var threadId: Int64? if let topMsgId = topMsgId { @@ -1780,6 +1780,8 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: updatedState.updateRevenueBalances(peerId: peer.peerId, balances: RevenueStats.Balances(apiRevenueBalances: balances)) case let .updateStarsBalance(balance): updatedState.updateStarsBalance(peerId: accountPeerId, balance: balance) + case let .updateStarsRevenueStatus(peer, status): + updatedState.updateStarsRevenueStatus(peerId: peer.peerId, status: StarsRevenueStats.Balances(apiStarsRevenueStatus: status)) default: break } @@ -3271,7 +3273,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) var currentAddQuickReplyMessages: OptimizeAddMessagesState? for operation in operations { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper, .UpdateRevenueBalances, .UpdateStarsBalance: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper, .UpdateRevenueBalances, .UpdateStarsBalance, .UpdateStarsRevenueStatus: if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) } @@ -3406,6 +3408,7 @@ func replayFinalState( var updateConfig = false var updatedRevenueBalances: [PeerId: RevenueStats.Balances] = [:] var updatedStarsBalance: [PeerId: Int64] = [:] + var updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:] var holesFromPreviousStateMessageIds: [MessageId] = [] var clearHolesFromPreviousStateForChannelMessagesWithPts: [PeerIdAndMessageNamespace: Int32] = [:] @@ -3882,7 +3885,13 @@ func replayFinalState( if let message = locallyRenderedMessage(message: message, peers: peers) { generatedEvent = reactionGeneratedEvent(previousMessage.reactionsAttribute, message.reactionsAttribute, message: message, transaction: transaction) } - return .update(message.withUpdatedLocalTags(updatedLocalTags).withUpdatedFlags(updatedFlags).withUpdatedAttributes(updatedAttributes)) + + var updatedMedia = message.media + if let previousPaidContent = previousMessage.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent, case .full = previousPaidContent.extendedMedia.first { + updatedMedia = previousMessage.media + } + + return .update(message.withUpdatedLocalTags(updatedLocalTags).withUpdatedFlags(updatedFlags).withUpdatedAttributes(updatedAttributes).withUpdatedMedia(updatedMedia)) }) if let generatedEvent = generatedEvent { addedReactionEvents.append(generatedEvent) @@ -4630,42 +4639,26 @@ func replayFinalState( transaction.updateMessage(messageId, update: { currentMessage in var media = currentMessage.media let invoice = media.first(where: { $0 is TelegramMediaInvoice }) as? TelegramMediaInvoice - let currentExtendedMedia = invoice?.extendedMedia + let paidContent = media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent var storeForwardInfo: StoreMessageForwardInfo? if let forwardInfo = currentMessage.forwardInfo { storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags) } - let updatedExtendedMedia: TelegramExtendedMedia? - switch apiExtendedMedia { - case let .messageExtendedMediaPreview(_, width, height, thumb, videoDuration): - var dimensions: PixelDimensions? - if let width = width, let height = height { - dimensions = PixelDimensions(width: width, height: height) - } - var immediateThumbnailData: Data? - if let thumb = thumb, case let .photoStrippedSize(_, bytes) = thumb { - immediateThumbnailData = bytes.makeData() - } - updatedExtendedMedia = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration) - case let .messageExtendedMedia(apiMedia): - let (media, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) - if let media = media { - updatedExtendedMedia = .full(media: media) - } else { - updatedExtendedMedia = currentExtendedMedia - } - } + let updatedExtendedMedia = apiExtendedMedia.compactMap { TelegramExtendedMedia(apiExtendedMedia: $0, peerId: messageId.peerId) } - if let updatedExtendedMedia = updatedExtendedMedia, var invoice = invoice { - if let currentExtendedMedia = currentExtendedMedia, case .full = currentExtendedMedia, case .preview = updatedExtendedMedia { - - } else { + if let first = updatedExtendedMedia.first, case .full = first { + if var invoice = invoice { media = media.filter { !($0 is TelegramMediaInvoice) } - invoice = invoice.withUpdatedExtendedMedia(updatedExtendedMedia) + invoice = invoice.withUpdatedExtendedMedia(first) media.append(invoice) } + if var paidContent = paidContent { + media = media.filter { !($0 is TelegramMediaPaidContent) } + paidContent = paidContent.withUpdatedExtendedMedia(updatedExtendedMedia) + media.append(paidContent) + } } return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: media)) @@ -4842,6 +4835,9 @@ func replayFinalState( updatedRevenueBalances[peerId] = balances case let .UpdateStarsBalance(peerId, balance): updatedStarsBalance[peerId] = balance + case let .UpdateStarsRevenueStatus(peerId, status): + updatedStarsRevenueStatus[peerId] = status + } } @@ -5336,5 +5332,5 @@ func replayFinalState( } } - return AccountReplayedFinalState(state: finalState, addedIncomingMessageIds: addedIncomingMessageIds, addedReactionEvents: addedReactionEvents, wasScheduledMessageIds: wasScheduledMessageIds, addedSecretMessageIds: addedSecretMessageIds, deletedMessageIds: deletedMessageIds, updatedTypingActivities: updatedTypingActivities, updatedWebpages: updatedWebpages, updatedCalls: updatedCalls, addedCallSignalingData: addedCallSignalingData, updatedGroupCallParticipants: updatedGroupCallParticipants, storyUpdates: storyUpdates, updatedPeersNearby: updatedPeersNearby, isContactUpdates: isContactUpdates, delayNotificatonsUntil: delayNotificatonsUntil, updatedIncomingThreadReadStates: updatedIncomingThreadReadStates, updatedOutgoingThreadReadStates: updatedOutgoingThreadReadStates, updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: updatedRevenueBalances, updatedStarsBalance: updatedStarsBalance) + return AccountReplayedFinalState(state: finalState, addedIncomingMessageIds: addedIncomingMessageIds, addedReactionEvents: addedReactionEvents, wasScheduledMessageIds: wasScheduledMessageIds, addedSecretMessageIds: addedSecretMessageIds, deletedMessageIds: deletedMessageIds, updatedTypingActivities: updatedTypingActivities, updatedWebpages: updatedWebpages, updatedCalls: updatedCalls, addedCallSignalingData: addedCallSignalingData, updatedGroupCallParticipants: updatedGroupCallParticipants, storyUpdates: storyUpdates, updatedPeersNearby: updatedPeersNearby, isContactUpdates: isContactUpdates, delayNotificatonsUntil: delayNotificatonsUntil, updatedIncomingThreadReadStates: updatedIncomingThreadReadStates, updatedOutgoingThreadReadStates: updatedOutgoingThreadReadStates, updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: updatedRevenueBalances, updatedStarsBalance: updatedStarsBalance, updatedStarsRevenueStatus: updatedStarsRevenueStatus) } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index da4b4d97994..9fa6beed64b 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -52,6 +52,10 @@ private final class UpdatedStarsBalanceSubscriberContext { let subscribers = Bag<([PeerId: Int64]) -> Void>() } +private final class UpdatedStarsRevenueStatusSubscriberContext { + let subscribers = Bag<([PeerId: StarsRevenueStats.Balances]) -> Void>() +} + public enum DeletedMessageId: Hashable { case global(Int32) case messageId(MessageId) @@ -287,6 +291,7 @@ public final class AccountStateManager { private var updatedPeersNearbyContext = UpdatedPeersNearbySubscriberContext() private var updatedRevenueBalancesContext = UpdatedRevenueBalancesSubscriberContext() private var updatedStarsBalanceContext = UpdatedStarsBalanceSubscriberContext() + private var updatedStarsRevenueStatusContext = UpdatedStarsRevenueStatusSubscriberContext() private let delayNotificatonsUntil = Atomic(value: nil) private let appliedMaxMessageIdPromise = Promise(nil) @@ -1038,6 +1043,9 @@ public final class AccountStateManager { if !events.updatedStarsBalance.isEmpty { strongSelf.notifyUpdatedStarsBalance(events.updatedStarsBalance) } + if !events.updatedStarsRevenueStatus.isEmpty { + strongSelf.notifyUpdatedStarsRevenueStatus(events.updatedStarsRevenueStatus) + } if !events.updatedCalls.isEmpty { for call in events.updatedCalls { strongSelf.callSessionManager?.updateSession(call, completion: { _ in }) @@ -1664,6 +1672,33 @@ public final class AccountStateManager { } } + public func updatedStarsRevenueStatus() -> Signal<[PeerId: StarsRevenueStats.Balances], NoError> { + let queue = self.queue + return Signal { [weak self] subscriber in + let disposable = MetaDisposable() + queue.async { + if let strongSelf = self { + let index = strongSelf.updatedStarsRevenueStatusContext.subscribers.add({ revenueBalances in + subscriber.putNext(revenueBalances) + }) + + disposable.set(ActionDisposable { + if let strongSelf = self { + strongSelf.updatedStarsRevenueStatusContext.subscribers.remove(index) + } + }) + } + } + return disposable + } + } + + private func notifyUpdatedStarsRevenueStatus(_ updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances]) { + for subscriber in self.updatedStarsRevenueStatusContext.subscribers.copyItems() { + subscriber(updatedStarsRevenueStatus) + } + } + func notifyDeletedMessages(messageIds: [MessageId]) { self.deletedMessagesPipe.putNext(messageIds.map { .messageId($0) }) } @@ -1963,6 +1998,12 @@ public final class AccountStateManager { } } + public func updatedStarsRevenueStatus() -> Signal<[PeerId: StarsRevenueStats.Balances], NoError> { + return self.impl.signalWith { impl, subscriber in + return impl.updatedStarsRevenueStatus().start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) + } + } + func addCustomOperation(_ f: Signal) -> Signal { return self.impl.signalWith { impl, subscriber in return impl.addCustomOperation(f).start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 1e9d4f8d646..432ed9bf486 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -1962,7 +1962,7 @@ public final class AccountViewTracker { public func scheduledMessagesViewForLocation(_ chatLocation: ChatLocationInput, additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { if let account = self.account { - let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 200, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .just(Namespaces.Message.allScheduled), orderStatistics: [], additionalData: additionalData) + let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: 200, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .just(Namespaces.Message.allScheduled), orderStatistics: [], additionalData: additionalData) return withState(signal, { [weak self] () -> Int32 in if let strongSelf = self { return OSAtomicIncrement32(&strongSelf.nextViewId) @@ -1995,7 +1995,7 @@ public final class AccountViewTracker { return .never() } let chatLocation: ChatLocationInput = .peer(peerId: account.peerId, threadId: Int64(quickReplyId)) - let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 200, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .just(Namespaces.Message.allQuickReply), orderStatistics: [], additionalData: additionalData) + let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: 200, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .just(Namespaces.Message.allQuickReply), orderStatistics: [], additionalData: additionalData) return withState(signal, { [weak self] () -> Int32 in if let strongSelf = self { return OSAtomicIncrement32(&strongSelf.nextViewId) @@ -2025,7 +2025,7 @@ public final class AccountViewTracker { return .never() } let chatLocation: ChatLocationInput = .peer(peerId: account.peerId, threadId: nil) - let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 200, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .just([Namespaces.Message.QuickReplyLocal]), orderStatistics: [], additionalData: []) + let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: 200, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .just([Namespaces.Message.QuickReplyLocal]), orderStatistics: [], additionalData: []) |> map { view, update, initialData in var entries: [MessageHistoryEntry] = [] for entry in view.entries { @@ -2049,7 +2049,7 @@ public final class AccountViewTracker { return signal } - public func aroundMessageOfInterestHistoryViewForLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange? = nil, count: Int, tag: HistoryViewInputTag? = nil, appendMessagesFromTheSameGroup: Bool = false, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = [], useRootInterfaceStateForThread: Bool = false) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + public func aroundMessageOfInterestHistoryViewForLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange? = nil, ignoreMessageIds: Set = Set(), count: Int, tag: HistoryViewInputTag? = nil, appendMessagesFromTheSameGroup: Bool = false, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = [], useRootInterfaceStateForThread: Bool = false) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { if let account = self.account { let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> if let peerId = chatLocation.peerId, let threadId = chatLocation.threadId, tag == nil { @@ -2074,6 +2074,7 @@ public final class AccountViewTracker { chatLocation, anchor: anchor, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, + ignoreMessageIds: ignoreMessageIds, count: count, fixedCombinedReadStates: .peer([peerId: CombinedPeerReadState(states: [ (Namespaces.Message.Cloud, PeerReadState.idBased(maxIncomingReadId: Int32.max - 1, maxOutgoingReadId: Int32.max - 1, maxKnownId: Int32.max - 1, count: 0, markedUnread: false)) @@ -2102,6 +2103,7 @@ public final class AccountViewTracker { chatLocation, anchor: anchor, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, + ignoreMessageIds: ignoreMessageIds, count: count, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], @@ -2115,10 +2117,10 @@ public final class AccountViewTracker { } } - return account.postbox.aroundMessageOfInterestHistoryViewForChatLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, customUnreadMessageId: nil, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) + return account.postbox.aroundMessageOfInterestHistoryViewForChatLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, count: count, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, customUnreadMessageId: nil, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) } } else { - signal = account.postbox.aroundMessageOfInterestHistoryViewForChatLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, customUnreadMessageId: nil, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) + signal = account.postbox.aroundMessageOfInterestHistoryViewForChatLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, count: count, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, customUnreadMessageId: nil, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) } return wrappedMessageHistorySignal(chatLocation: chatLocation, signal: signal, fixedCombinedReadStates: nil, addHoleIfNeeded: true) } else { @@ -2126,16 +2128,16 @@ public final class AccountViewTracker { } } - public func aroundIdMessageHistoryViewForLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange? = nil, count: Int, ignoreRelatedChats: Bool, messageId: MessageId, tag: HistoryViewInputTag? = nil, appendMessagesFromTheSameGroup: Bool = false, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = [], useRootInterfaceStateForThread: Bool = false) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + public func aroundIdMessageHistoryViewForLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange? = nil, ignoreMessageIds: Set = Set(), count: Int, ignoreRelatedChats: Bool, messageId: MessageId, tag: HistoryViewInputTag? = nil, appendMessagesFromTheSameGroup: Bool = false, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = [], useRootInterfaceStateForThread: Bool = false) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { if let account = self.account { - let signal = account.postbox.aroundIdMessageHistoryViewForLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, ignoreRelatedChats: ignoreRelatedChats, messageId: messageId, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) + let signal = account.postbox.aroundIdMessageHistoryViewForLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, count: count, ignoreRelatedChats: ignoreRelatedChats, messageId: messageId, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) return wrappedMessageHistorySignal(chatLocation: chatLocation, signal: signal, fixedCombinedReadStates: nil, addHoleIfNeeded: false) } else { return .never() } } - public func aroundMessageHistoryViewForLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange? = nil, index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, count: Int, clipHoles: Bool = true, ignoreRelatedChats: Bool = false, fixedCombinedReadStates: MessageHistoryViewReadState?, tag: HistoryViewInputTag? = nil, appendMessagesFromTheSameGroup: Bool = false, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = [], useRootInterfaceStateForThread: Bool = false) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + public func aroundMessageHistoryViewForLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange? = nil, ignoreMessageIds: Set = Set(), index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, count: Int, clipHoles: Bool = true, ignoreRelatedChats: Bool = false, fixedCombinedReadStates: MessageHistoryViewReadState?, tag: HistoryViewInputTag? = nil, appendMessagesFromTheSameGroup: Bool = false, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = [], useRootInterfaceStateForThread: Bool = false) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { if let account = self.account { let inputAnchor: HistoryViewInputAnchor switch index { @@ -2146,7 +2148,7 @@ public final class AccountViewTracker { case let .message(index): inputAnchor = .index(index) } - let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: inputAnchor, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, clipHoles: clipHoles, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) + let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: inputAnchor, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, count: count, clipHoles: clipHoles, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) return wrappedMessageHistorySignal(chatLocation: chatLocation, signal: signal, fixedCombinedReadStates: fixedCombinedReadStates, addHoleIfNeeded: false) } else { return .never() diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index d53abc3f954..edf64213f0d 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -47,6 +47,12 @@ func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox, force: if (force || fromFile.size == toFile.size || fromFile.resource.size == toFile.resource.size) && fromFile.mimeType == toFile.mimeType { copyOrMoveResourceData(from: fromFile.resource, to: toFile.resource, mediaBox: postbox.mediaBox) } + } else if let fromPaidContent = from as? TelegramMediaPaidContent, let toPaidContent = to as? TelegramMediaPaidContent { + for (fromMedia, toMedia) in zip(fromPaidContent.extendedMedia, toPaidContent.extendedMedia) { + if case let .full(fullFromMedia) = fromMedia, case let .full(fullToMedia) = toMedia { + applyMediaResourceChanges(from: fullFromMedia, to: fullToMedia, postbox: postbox, force: force) + } + } } } diff --git a/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift index 3f85200f212..3277233f4b5 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift @@ -211,7 +211,7 @@ private func synchronizeChatInputState(transaction: Transaction, postbox: Postbo replyTo = .inputReplyToMessage(flags: innerFlags, replyToMsgId: topMsgId, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil) } - return network.request(Api.functions.messages.saveDraft(flags: flags, replyTo: replyTo, peer: inputPeer, message: inputState?.text ?? "", entities: apiEntitiesFromMessageTextEntities(inputState?.entities ?? [], associatedPeers: SimpleDictionary()), media: nil)) + return network.request(Api.functions.messages.saveDraft(flags: flags, replyTo: replyTo, peer: inputPeer, message: inputState?.text ?? "", entities: apiEntitiesFromMessageTextEntities(inputState?.entities ?? [], associatedPeers: SimpleDictionary()), media: nil, effect: nil)) |> delay(2.0, queue: Queue.concurrentDefaultQueue()) |> `catch` { _ -> Signal in return .single(.boolFalse) diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index a5e1b25ed47..ff6734e87fc 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -6,8 +6,23 @@ import MtProtoKit public struct PendingMessageStatus: Equatable { + public struct Progress: Equatable { + public let progress: Float + public let mediaProgress: [MediaId: Float] + + public init(progress: Float, mediaProgress: [MediaId: Float] = [:]) { + self.progress = progress + self.mediaProgress = mediaProgress + } + + init(_ contentProgress: PendingMessageUploadedContentProgress) { + self.progress = contentProgress.progress + self.mediaProgress = contentProgress.mediaProgress + } + } + public let isRunning: Bool - public let progress: Float + public let progress: Progress } private enum PendingMessageState { @@ -364,7 +379,7 @@ public final class PendingMessageManager { self.messageContexts[id] = messageContext } - let status = PendingMessageStatus(isRunning: false, progress: 0.0) + let status = PendingMessageStatus(isRunning: false, progress: PendingMessageStatus.Progress(progress: 0.0)) if status != messageContext.status { messageContext.status = status for subscriber in messageContext.statusSubscribers.copyItems() { @@ -618,7 +633,7 @@ public final class PendingMessageManager { switch next { case let .progress(progress): if let current = strongSelf.messageContexts[messageId] { - let status = PendingMessageStatus(isRunning: true, progress: progress) + let status = PendingMessageStatus(isRunning: true, progress: PendingMessageStatus.Progress(progress: progress)) current.status = status for subscriber in current.statusSubscribers.copyItems() { subscriber(current.status, current.error) @@ -636,7 +651,7 @@ public final class PendingMessageManager { private func beginUploadingMessage(messageContext: PendingMessageContext, id: MessageId, threadId: Int64?, groupId: Int64?, uploadSignal: Signal) { messageContext.state = .uploading(groupId: groupId) - let status = PendingMessageStatus(isRunning: true, progress: 0.0) + let status = PendingMessageStatus(isRunning: true, progress: PendingMessageStatus.Progress(progress: 0.0)) messageContext.status = status for subscriber in messageContext.statusSubscribers.copyItems() { subscriber(messageContext.status, messageContext.error) @@ -678,7 +693,7 @@ public final class PendingMessageManager { switch next { case let .progress(progress): if let current = strongSelf.messageContexts[id] { - let status = PendingMessageStatus(isRunning: true, progress: progress) + let status = PendingMessageStatus(isRunning: true, progress: PendingMessageStatus.Progress(progress)) current.status = status for subscriber in current.statusSubscribers.copyItems() { subscriber(current.status, current.error) @@ -712,7 +727,7 @@ public final class PendingMessageManager { if case let .waitingForUploadToStart(groupId, uploadSignal) = context.state { if self.canBeginUploadingMessage(id: contextId, type: context.contentType ?? .media) { context.state = .uploading(groupId: groupId) - let status = PendingMessageStatus(isRunning: true, progress: 0.0) + let status = PendingMessageStatus(isRunning: true, progress: PendingMessageStatus.Progress(progress: 0.0)) context.status = status for subscriber in context.statusSubscribers.copyItems() { subscriber(context.status, context.error) @@ -734,7 +749,7 @@ public final class PendingMessageManager { switch next { case let .progress(progress): if let current = strongSelf.messageContexts[contextId] { - let status = PendingMessageStatus(isRunning: true, progress: progress) + let status = PendingMessageStatus(isRunning: true, progress: PendingMessageStatus.Progress(progress)) current.status = status for subscriber in current.statusSubscribers.copyItems() { subscriber(context.status, context.error) @@ -1044,29 +1059,67 @@ public final class PendingMessageManager { |> `catch` { error -> Signal in return deferred { if let strongSelf = self { - if error.errorDescription.hasPrefix("FILEREF_INVALID") || error.errorDescription.hasPrefix("FILE_REFERENCE_") { - var allFoundAndValid = true - for (message, _) in messages { - if let context = strongSelf.messageContexts[message.id] { - if context.forcedReuploadOnce { - allFoundAndValid = false - break - } - } else { - allFoundAndValid = false - break + let errorText: String = error.errorDescription + + if errorText.hasPrefix("FILEREF_INVALID") || errorText.hasPrefix("FILE_REFERENCE_") { + var selectiveIndices: [Int]? + if errorText.hasPrefix("FILE_REFERENCE_") && errorText.hasSuffix("_EXPIRED") { + if let value = Int(errorText[errorText.index(errorText.startIndex, offsetBy: "FILE_REFERENCE_".count).. UInt { - return 181 + return 183 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index b088ef293d2..fc245b1f575 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -23,6 +23,7 @@ public struct UserLimitsConfiguration: Equatable { public var maxStoriesWeeklyCount: Int32 public var maxStoriesMonthlyCount: Int32 public var maxStoriesSuggestedReactions: Int32 + public var maxStoriesLinksCount: Int32 public var maxGiveawayChannelsCount: Int32 public var maxGiveawayCountriesCount: Int32 public var maxGiveawayPeriodSeconds: Int32 @@ -51,6 +52,7 @@ public struct UserLimitsConfiguration: Equatable { maxStoriesWeeklyCount: 7, maxStoriesMonthlyCount: 30, maxStoriesSuggestedReactions: 1, + maxStoriesLinksCount: 3, maxGiveawayChannelsCount: 10, maxGiveawayCountriesCount: 10, maxGiveawayPeriodSeconds: 86400 * 31, @@ -80,6 +82,7 @@ public struct UserLimitsConfiguration: Equatable { maxStoriesWeeklyCount: Int32, maxStoriesMonthlyCount: Int32, maxStoriesSuggestedReactions: Int32, + maxStoriesLinksCount: Int32, maxGiveawayChannelsCount: Int32, maxGiveawayCountriesCount: Int32, maxGiveawayPeriodSeconds: Int32, @@ -106,6 +109,7 @@ public struct UserLimitsConfiguration: Equatable { self.maxStoriesWeeklyCount = maxStoriesWeeklyCount self.maxStoriesMonthlyCount = maxStoriesMonthlyCount self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions + self.maxStoriesLinksCount = maxStoriesLinksCount self.maxGiveawayChannelsCount = maxGiveawayChannelsCount self.maxGiveawayCountriesCount = maxGiveawayCountriesCount self.maxGiveawayPeriodSeconds = maxGiveawayPeriodSeconds @@ -158,6 +162,7 @@ extension UserLimitsConfiguration { self.maxStoriesWeeklyCount = getValue("stories_sent_weekly_limit", orElse: defaultValue.maxStoriesWeeklyCount) self.maxStoriesMonthlyCount = getValue("stories_sent_monthly_limit", orElse: defaultValue.maxStoriesMonthlyCount) self.maxStoriesSuggestedReactions = getValue("stories_suggested_reactions_limit", orElse: defaultValue.maxStoriesMonthlyCount) + self.maxStoriesLinksCount = getGeneralValue("stories_area_url_max", orElse: defaultValue.maxStoriesLinksCount) self.maxGiveawayChannelsCount = getGeneralValue("giveaway_add_peers_max", orElse: defaultValue.maxGiveawayChannelsCount) self.maxGiveawayCountriesCount = getGeneralValue("giveaway_countries_max", orElse: defaultValue.maxGiveawayCountriesCount) self.maxGiveawayPeriodSeconds = getGeneralValue("giveaway_period_max", orElse: defaultValue.maxGiveawayPeriodSeconds) diff --git a/submodules/TelegramCore/Sources/Statistics/StarsRevenueStatistics.swift b/submodules/TelegramCore/Sources/Statistics/StarsRevenueStatistics.swift new file mode 100644 index 00000000000..09cf65e4c7b --- /dev/null +++ b/submodules/TelegramCore/Sources/Statistics/StarsRevenueStatistics.swift @@ -0,0 +1,350 @@ +import Foundation +import SwiftSignalKit +import Postbox +import TelegramApi +import MtProtoKit + +public struct StarsRevenueStats: Equatable { + public struct Balances: Equatable { + public let currentBalance: Int64 + public let availableBalance: Int64 + public let overallRevenue: Int64 + public let withdrawEnabled: Bool + public let nextWithdrawalTimestamp: Int32? + } + + public let revenueGraph: StatsGraph + public let balances: Balances + public let usdRate: Double + + init(revenueGraph: StatsGraph, balances: Balances, usdRate: Double) { + self.revenueGraph = revenueGraph + self.balances = balances + self.usdRate = usdRate + } + + public static func == (lhs: StarsRevenueStats, rhs: StarsRevenueStats) -> Bool { + if lhs.revenueGraph != rhs.revenueGraph { + return false + } + if lhs.balances != rhs.balances { + return false + } + if lhs.usdRate != rhs.usdRate { + return false + } + return true + } +} + +public extension StarsRevenueStats { + func withUpdated(balances: StarsRevenueStats.Balances) -> StarsRevenueStats { + return StarsRevenueStats( + revenueGraph: self.revenueGraph, + balances: balances, + usdRate: self.usdRate + ) + } +} + +extension StarsRevenueStats { + init(apiStarsRevenueStats: Api.payments.StarsRevenueStats, peerId: PeerId) { + switch apiStarsRevenueStats { + case let .starsRevenueStats(revenueGraph, balances, usdRate): + self.init(revenueGraph: StatsGraph(apiStatsGraph: revenueGraph), balances: StarsRevenueStats.Balances(apiStarsRevenueStatus: balances), usdRate: usdRate) + } + } +} + +extension StarsRevenueStats.Balances { + init(apiStarsRevenueStatus: Api.StarsRevenueStatus) { + switch apiStarsRevenueStatus { + case let .starsRevenueStatus(flags, currentBalance, availableBalance, overallRevenue, nextWithdrawalAt): + self.init(currentBalance: currentBalance, availableBalance: availableBalance, overallRevenue: overallRevenue, withdrawEnabled: ((flags & (1 << 0)) != 0), nextWithdrawalTimestamp: nextWithdrawalAt) + } + } +} + +public struct StarsRevenueStatsContextState: Equatable { + public var stats: StarsRevenueStats? +} + +private func requestStarsRevenueStats(postbox: Postbox, network: Network, peerId: PeerId, dark: Bool = false) -> Signal { + return postbox.transaction { transaction -> Peer? in + if let peer = transaction.getPeer(peerId) { + return peer + } + return nil + } |> mapToSignal { peer -> Signal in + guard let peer, let inputPeer = apiInputPeer(peer) else { + return .never() + } + + var flags: Int32 = 0 + if dark { + flags |= (1 << 1) + } + + return network.request(Api.functions.payments.getStarsRevenueStats(flags: flags, peer: inputPeer)) + |> map { result -> StarsRevenueStats? in + return StarsRevenueStats(apiStarsRevenueStats: result, peerId: peerId) + } + |> retryRequest + } +} + +private final class StarsRevenueStatsContextImpl { + private let account: Account + private let peerId: PeerId + + private var _state: StarsRevenueStatsContextState { + didSet { + if self._state != oldValue { + self._statePromise.set(.single(self._state)) + } + } + } + private let _statePromise = Promise() + var state: Signal { + return self._statePromise.get() + } + + private let disposable = MetaDisposable() + private let updateDisposable = MetaDisposable() + + init(account: Account, peerId: PeerId) { + assert(Queue.mainQueue().isCurrent()) + + self.account = account + self.peerId = peerId + self._state = StarsRevenueStatsContextState(stats: nil) + self._statePromise.set(.single(self._state)) + + self.load() + } + + deinit { + assert(Queue.mainQueue().isCurrent()) + self.disposable.dispose() + self.updateDisposable.dispose() + } + + public func setUpdated(_ f: @escaping () -> Void) { + let peerId = self.peerId + self.updateDisposable.set((account.stateManager.updatedStarsRevenueStatus() + |> deliverOnMainQueue).startStrict(next: { updates in + if let _ = updates[peerId] { + f() + } + })) + } + + fileprivate func load() { + assert(Queue.mainQueue().isCurrent()) + + let account = self.account + let peerId = self.peerId + let signal = requestStarsRevenueStats(postbox: self.account.postbox, network: self.account.network, peerId: self.peerId) + |> mapToSignal { initial -> Signal in + guard let initial else { + return .single(nil) + } + return .single(initial) + |> then( + account.stateManager.updatedStarsRevenueStatus() + |> mapToSignal { updates in + if let balances = updates[peerId] { + return .single(initial.withUpdated(balances: balances)) + } + return .complete() + } + ) + } + + self.disposable.set((signal + |> deliverOnMainQueue).start(next: { [weak self] stats in + if let strongSelf = self { + strongSelf._state = StarsRevenueStatsContextState(stats: stats) + strongSelf._statePromise.set(.single(strongSelf._state)) + } + })) + } + + func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal { + if let token = graph.token { + return requestGraph(postbox: self.account.postbox, network: self.account.network, peerId: self.peerId, token: token, x: x) + } else { + return .single(nil) + } + } +} + +public final class StarsRevenueStatsContext { + private let impl: QueueLocalObject + + public var state: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.state.start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } + + public init(account: Account, peerId: PeerId) { + self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { + return StarsRevenueStatsContextImpl(account: account, peerId: peerId) + }) + } + + public func setUpdated(_ f: @escaping () -> Void) { + self.impl.with { impl in + impl.setUpdated(f) + } + } + + public func reload() { + self.impl.with { impl in + impl.load() + } + } + + public func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.loadDetailedGraph(graph, x: x).start(next: { value in + subscriber.putNext(value) + subscriber.putCompletion() + })) + } + return disposable + } + } +} + +public enum RequestStarsRevenueWithdrawalError : Equatable { + case generic + case twoStepAuthMissing + case twoStepAuthTooFresh(Int32) + case authSessionTooFresh(Int32) + case limitExceeded + case requestPassword + case invalidPassword + case serverProvided(text: String) +} + +func _internal_checkStarsRevenueWithdrawalAvailability(account: Account) -> Signal { + return account.network.request(Api.functions.payments.getStarsRevenueWithdrawalUrl(peer: .inputPeerEmpty, stars: 0, password: .inputCheckPasswordEmpty)) + |> mapError { error -> RequestStarsRevenueWithdrawalError in + if error.errorDescription == "PASSWORD_HASH_INVALID" { + return .requestPassword + } else if error.errorDescription == "PASSWORD_MISSING" { + return .twoStepAuthMissing + } else if error.errorDescription.hasPrefix("PASSWORD_TOO_FRESH_") { + let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "PASSWORD_TOO_FRESH_".count)...]) + if let value = Int32(timeout) { + return .twoStepAuthTooFresh(value) + } + } else if error.errorDescription.hasPrefix("SESSION_TOO_FRESH_") { + let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "SESSION_TOO_FRESH_".count)...]) + if let value = Int32(timeout) { + return .authSessionTooFresh(value) + } + } + return .generic + } + |> ignoreValues +} + +func _internal_requestStarsRevenueWithdrawalUrl(account: Account, peerId: PeerId, amount: Int64, password: String) -> Signal { + guard !password.isEmpty else { + return .fail(.invalidPassword) + } + + return account.postbox.transaction { transaction -> Signal in + guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { + return .fail(.generic) + } + + let checkPassword = _internal_twoStepAuthData(account.network) + |> mapError { error -> RequestStarsRevenueWithdrawalError in + if error.errorDescription.hasPrefix("FLOOD_WAIT") { + return .limitExceeded + } else { + return .generic + } + } + |> mapToSignal { authData -> Signal in + if let currentPasswordDerivation = authData.currentPasswordDerivation, let srpSessionData = authData.srpSessionData { + guard let kdfResult = passwordKDF(encryptionProvider: account.network.encryptionProvider, password: password, derivation: currentPasswordDerivation, srpSessionData: srpSessionData) else { + return .fail(.generic) + } + return .single(.inputCheckPasswordSRP(srpId: kdfResult.id, A: Buffer(data: kdfResult.A), M1: Buffer(data: kdfResult.M1))) + } else { + return .fail(.twoStepAuthMissing) + } + } + + return checkPassword + |> mapToSignal { password -> Signal in + return account.network.request(Api.functions.payments.getStarsRevenueWithdrawalUrl(peer: inputPeer, stars: amount, password: password), automaticFloodWait: false) + |> mapError { error -> RequestStarsRevenueWithdrawalError in + if error.errorCode == 406 { + return .serverProvided(text: error.errorDescription) + } else if error.errorDescription.hasPrefix("FLOOD_WAIT") { + return .limitExceeded + } else if error.errorDescription == "PASSWORD_HASH_INVALID" { + return .invalidPassword + } else if error.errorDescription == "PASSWORD_MISSING" { + return .twoStepAuthMissing + } else if error.errorDescription.hasPrefix("PASSWORD_TOO_FRESH_") { + let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "PASSWORD_TOO_FRESH_".count)...]) + if let value = Int32(timeout) { + return .twoStepAuthTooFresh(value) + } + } else if error.errorDescription.hasPrefix("SESSION_TOO_FRESH_") { + let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "SESSION_TOO_FRESH_".count)...]) + if let value = Int32(timeout) { + return .authSessionTooFresh(value) + } + } + return .generic + } + |> map { result -> String in + switch result { + case let .starsRevenueWithdrawalUrl(url): + return url + } + } + } + } + |> mapError { _ -> RequestStarsRevenueWithdrawalError in } + |> switchToLatest +} + +func _internal_requestStarsRevenueAdsAccountlUrl(account: Account, peerId: EnginePeer.Id) -> Signal { + return account.postbox.transaction { transaction -> Signal in + guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { + return .single(nil) + } + return account.network.request(Api.functions.payments.getStarsRevenueAdsAccountUrl(peer: inputPeer)) + |> map(Optional.init) + |> `catch` { error -> Signal in + return .single(nil) + } + |> map { result -> String? in + guard let result else { + return nil + } + switch result { + case let .starsRevenueAdsAccountUrl(url): + return url + } + } + } + |> switchToLatest +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift index 1e2e7e90002..8ca472a617e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift @@ -22,6 +22,7 @@ public struct CachedChannelFlags: OptionSet { public static let translationHidden = CachedChannelFlags(rawValue: 1 << 8) public static let adsRestricted = CachedChannelFlags(rawValue: 1 << 9) public static let canViewRevenue = CachedChannelFlags(rawValue: 1 << 10) + public static let paidMediaAllowed = CachedChannelFlags(rawValue: 1 << 11) } public struct CachedChannelParticipantsSummary: PostboxCoding, Equatable { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_NetworkSettings.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_NetworkSettings.swift index 420f7cda947..60f9ac3735c 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_NetworkSettings.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_NetworkSettings.swift @@ -26,7 +26,7 @@ public struct NetworkSettings: Codable { self.applicationUpdateUrlPrefix = try? container.decodeIfPresent(String.self, forKey: "applicationUpdateUrlPrefix") self.backupHostOverride = try? container.decodeIfPresent(String.self, forKey: "backupHostOverride") self.useNetworkFramework = try container.decodeIfPresent(Bool.self, forKey: "useNetworkFramework_v2") - self.useExperimentalDownload = try container.decodeIfPresent(Bool.self, forKey: "useExperimentalDownload") + self.useExperimentalDownload = try container.decodeIfPresent(Bool.self, forKey: "useExperimentalDownload_v2") } public func encode(to encoder: Encoder) throws { @@ -36,6 +36,6 @@ public struct NetworkSettings: Codable { try container.encodeIfPresent(self.applicationUpdateUrlPrefix, forKey: "applicationUpdateUrlPrefix") try container.encodeIfPresent(self.backupHostOverride, forKey: "backupHostOverride") try container.encodeIfPresent(self.useNetworkFramework, forKey: "useNetworkFramework_v2") - try container.encodeIfPresent(self.useExperimentalDownload, forKey: "useExperimentalDownload") + try container.encodeIfPresent(self.useExperimentalDownload, forKey: "useExperimentalDownload_v2") } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift index f3673e84a0d..6758c551202 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift @@ -3,6 +3,12 @@ import Postbox import TelegramApi public struct MessageReaction: Equatable, PostboxCoding, Codable { + #if DEBUG + public static let starsReactionId: Int64 = 5435957248314579621 + #else + public static let starsReactionId: Int64 = 12340000 + #endif + public enum Reaction: Hashable, Comparable, Codable, PostboxCoding { case builtin(String) case custom(Int64) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift index c802fb9a4cb..32d49fc4fd7 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift @@ -7,13 +7,15 @@ public struct SynchronizeableChatInputState: Codable, Equatable { public let entities: [MessageTextEntity] public let timestamp: Int32 public let textSelection: Range? + public let messageEffectId: Int64? - public init(replySubject: EngineMessageReplySubject?, text: String, entities: [MessageTextEntity], timestamp: Int32, textSelection: Range?) { + public init(replySubject: EngineMessageReplySubject?, text: String, entities: [MessageTextEntity], timestamp: Int32, textSelection: Range?, messageEffectId: Int64?) { self.replySubject = replySubject self.text = text self.entities = entities self.timestamp = timestamp self.textSelection = textSelection + self.messageEffectId = messageEffectId } public init(from decoder: Decoder) throws { @@ -32,6 +34,7 @@ public struct SynchronizeableChatInputState: Codable, Equatable { } } self.textSelection = nil + self.messageEffectId = try container.decodeIfPresent(Int64.self, forKey: "messageEffectId") } public func encode(to encoder: Encoder) throws { @@ -41,6 +44,7 @@ public struct SynchronizeableChatInputState: Codable, Equatable { try container.encode(self.entities, forKey: "e") try container.encode(self.timestamp, forKey: "s") try container.encodeIfPresent(self.replySubject, forKey: "rep") + try container.encodeIfPresent(self.messageEffectId, forKey: "messageEffectId") } public static func ==(lhs: SynchronizeableChatInputState, rhs: SynchronizeableChatInputState) -> Bool { @@ -59,6 +63,9 @@ public struct SynchronizeableChatInputState: Codable, Equatable { if lhs.textSelection != rhs.textSelection { return false } + if lhs.messageEffectId != rhs.messageEffectId { + return false + } return true } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaMap.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaMap.swift index 2039d62d8b1..8592f4c6642 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaMap.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaMap.swift @@ -2,33 +2,28 @@ import Postbox public let liveLocationIndefinitePeriod: Int32 = 0x7fffffff -public final class NamedGeoPlace: PostboxCoding, Equatable { - public let country: String? +public final class MapGeoAddress: PostboxCoding, Equatable { + public let country: String public let state: String? public let city: String? - public let district: String? public let street: String? - public init(country: String?, state: String?, city: String?, district: String?, street: String?) { + public init(country: String, state: String?, city: String?, street: String?) { self.country = country self.state = state self.city = city - self.district = district self.street = street } public init(decoder: PostboxDecoder) { - self.country = decoder.decodeOptionalStringForKey("gp_co") + self.country = decoder.decodeStringForKey("gp_co", orElse: "") self.state = decoder.decodeOptionalStringForKey("gp_sta") self.city = decoder.decodeOptionalStringForKey("gp_ci") - self.district = decoder.decodeOptionalStringForKey("gp_dis") self.street = decoder.decodeOptionalStringForKey("gp_str") } public func encode(_ encoder: PostboxEncoder) { - if let country = self.country { - encoder.encodeString(country, forKey: "gp_co") - } + encoder.encodeString(country, forKey: "gp_co") if let state = self.state { encoder.encodeString(state, forKey: "gp_sta") @@ -38,16 +33,12 @@ public final class NamedGeoPlace: PostboxCoding, Equatable { encoder.encodeString(city, forKey: "gp_ci") } - if let district = self.district { - encoder.encodeString(district, forKey: "gp_dis") - } - if let street = self.street { encoder.encodeString(street, forKey: "gp_str") } } - public static func ==(lhs: NamedGeoPlace, rhs: NamedGeoPlace) -> Bool { + public static func ==(lhs: MapGeoAddress, rhs: MapGeoAddress) -> Bool { if lhs.country != rhs.country { return false } @@ -57,9 +48,6 @@ public final class NamedGeoPlace: PostboxCoding, Equatable { if lhs.city != rhs.city { return false } - if lhs.district != rhs.district { - return false - } if lhs.street != rhs.street { return false } @@ -137,21 +125,21 @@ public final class TelegramMediaMap: Media, Equatable { public let longitude: Double public let heading: Int32? public let accuracyRadius: Double? - public let geoPlace: NamedGeoPlace? public let venue: MapVenue? + public let address: MapGeoAddress? public let liveBroadcastingTimeout: Int32? public let liveProximityNotificationRadius: Int32? public let id: MediaId? = nil public let peerIds: [PeerId] = [] - public init(latitude: Double, longitude: Double, heading: Int32?, accuracyRadius: Double?, geoPlace: NamedGeoPlace?, venue: MapVenue?, liveBroadcastingTimeout: Int32?, liveProximityNotificationRadius: Int32?) { + public init(latitude: Double, longitude: Double, heading: Int32?, accuracyRadius: Double?, venue: MapVenue?, address: MapGeoAddress? = nil, liveBroadcastingTimeout: Int32? = nil, liveProximityNotificationRadius: Int32? = nil) { self.latitude = latitude self.longitude = longitude self.heading = heading self.accuracyRadius = accuracyRadius - self.geoPlace = geoPlace self.venue = venue + self.address = address self.liveBroadcastingTimeout = liveBroadcastingTimeout self.liveProximityNotificationRadius = liveProximityNotificationRadius } @@ -161,8 +149,8 @@ public final class TelegramMediaMap: Media, Equatable { self.longitude = decoder.decodeDoubleForKey("lo", orElse: 0.0) self.heading = decoder.decodeOptionalInt32ForKey("hdg") self.accuracyRadius = decoder.decodeOptionalDoubleForKey("acc") - self.geoPlace = decoder.decodeObjectForKey("gp", decoder: { NamedGeoPlace(decoder: $0) }) as? NamedGeoPlace self.venue = decoder.decodeObjectForKey("ve", decoder: { MapVenue(decoder: $0) }) as? MapVenue + self.address = decoder.decodeObjectForKey("adr", decoder: { MapGeoAddress(decoder: $0) }) as? MapGeoAddress self.liveBroadcastingTimeout = decoder.decodeOptionalInt32ForKey("bt") self.liveProximityNotificationRadius = decoder.decodeOptionalInt32ForKey("pnr") } @@ -180,16 +168,16 @@ public final class TelegramMediaMap: Media, Equatable { } else { encoder.encodeNil(forKey: "acc") } - if let geoPlace = self.geoPlace { - encoder.encodeObject(geoPlace, forKey: "gp") - } else { - encoder.encodeNil(forKey: "gp") - } if let venue = self.venue { encoder.encodeObject(venue, forKey: "ve") } else { encoder.encodeNil(forKey: "ve") } + if let address = self.address { + encoder.encodeObject(address, forKey: "adr") + } else { + encoder.encodeNil(forKey: "adr") + } if let liveBroadcastingTimeout = self.liveBroadcastingTimeout { encoder.encodeInt32(liveBroadcastingTimeout, forKey: "bt") } else { @@ -217,9 +205,6 @@ public final class TelegramMediaMap: Media, Equatable { if self.accuracyRadius != other.accuracyRadius { return false } - if self.geoPlace != other.geoPlace { - return false - } if self.venue != other.venue { return false } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaPaidContent.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaPaidContent.swift new file mode 100644 index 00000000000..c288675249c --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaPaidContent.swift @@ -0,0 +1,59 @@ +import Foundation +import Postbox + +public final class TelegramMediaPaidContent: Media, Equatable { + public var peerIds: [PeerId] = [] + + public var id: MediaId? { + return nil + } + + public let amount: Int64 + public let extendedMedia: [TelegramExtendedMedia] + + public init(amount: Int64, extendedMedia: [TelegramExtendedMedia]) { + self.amount = amount + self.extendedMedia = extendedMedia + } + + public init(decoder: PostboxDecoder) { + self.amount = decoder.decodeInt64ForKey("a", orElse: 0) + self.extendedMedia = (try? decoder.decodeObjectArrayWithCustomDecoderForKey("m", decoder: { TelegramExtendedMedia(decoder: $0) })) ?? [] + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt64(self.amount, forKey: "a") + encoder.encodeObjectArray(self.extendedMedia, forKey: "m") + } + + public static func ==(lhs: TelegramMediaPaidContent, rhs: TelegramMediaPaidContent) -> Bool { + return lhs.isEqual(to: rhs) + } + + public func isEqual(to other: Media) -> Bool { + guard let other = other as? TelegramMediaPaidContent else { + return false + } + + if self.amount != other.amount { + return false + } + + if self.extendedMedia != other.extendedMedia { + return false + } + + return true + } + + public func isSemanticallyEqual(to other: Media) -> Bool { + return self.isEqual(to: other) + } + + public func withUpdatedExtendedMedia(_ extendedMedia: [TelegramExtendedMedia]) -> TelegramMediaPaidContent { + return TelegramMediaPaidContent( + amount: self.amount, + extendedMedia: extendedMedia + ) + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/ChangeAccountPhoneNumber.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/ChangeAccountPhoneNumber.swift index 14e527f5983..7666b030d42 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/ChangeAccountPhoneNumber.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/ChangeAccountPhoneNumber.swift @@ -72,7 +72,7 @@ func _internal_requestChangeAccountPhoneNumberVerification(account: Account, api parsedNextType = AuthorizationCodeNextType(apiType: nextType) } - if case let .sentCodeTypeFirebaseSms(_, _, _, receipt, pushTimeout, _) = type { + if case let .sentCodeTypeFirebaseSms(_, _, _, _, receipt, pushTimeout, _) = type { return firebaseSecretStream |> map { mapping -> String? in guard let receipt = receipt else { @@ -147,7 +147,7 @@ private func internalResendChangeAccountPhoneNumberVerification(account: Account parsedNextType = AuthorizationCodeNextType(apiType: nextType) } - if case let .sentCodeTypeFirebaseSms(_, _, _, receipt, pushTimeout, _) = type { + if case let .sentCodeTypeFirebaseSms(_, _, _, _, receipt, pushTimeout, _) = type { return firebaseSecretStream |> map { mapping -> String? in guard let receipt = receipt else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift index 8107e653cd6..7a04b9c3d1f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift @@ -57,6 +57,7 @@ public enum EngineConfiguration { public let maxStoriesWeeklyCount: Int32 public let maxStoriesMonthlyCount: Int32 public let maxStoriesSuggestedReactions: Int32 + public let maxStoriesLinksCount: Int32 public let maxGiveawayChannelsCount: Int32 public let maxGiveawayCountriesCount: Int32 public let maxGiveawayPeriodSeconds: Int32 @@ -88,6 +89,7 @@ public enum EngineConfiguration { maxStoriesWeeklyCount: Int32, maxStoriesMonthlyCount: Int32, maxStoriesSuggestedReactions: Int32, + maxStoriesLinksCount: Int32, maxGiveawayChannelsCount: Int32, maxGiveawayCountriesCount: Int32, maxGiveawayPeriodSeconds: Int32, @@ -114,6 +116,7 @@ public enum EngineConfiguration { self.maxStoriesWeeklyCount = maxStoriesWeeklyCount self.maxStoriesMonthlyCount = maxStoriesMonthlyCount self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions + self.maxStoriesLinksCount = maxStoriesLinksCount self.maxGiveawayChannelsCount = maxGiveawayChannelsCount self.maxGiveawayCountriesCount = maxGiveawayCountriesCount self.maxGiveawayPeriodSeconds = maxGiveawayPeriodSeconds @@ -176,6 +179,7 @@ public extension EngineConfiguration.UserLimits { maxStoriesWeeklyCount: userLimitsConfiguration.maxStoriesWeeklyCount, maxStoriesMonthlyCount: userLimitsConfiguration.maxStoriesMonthlyCount, maxStoriesSuggestedReactions: userLimitsConfiguration.maxStoriesSuggestedReactions, + maxStoriesLinksCount: userLimitsConfiguration.maxStoriesLinksCount, maxGiveawayChannelsCount: userLimitsConfiguration.maxGiveawayChannelsCount, maxGiveawayCountriesCount: userLimitsConfiguration.maxGiveawayCountriesCount, maxGiveawayPeriodSeconds: userLimitsConfiguration.maxGiveawayPeriodSeconds, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index f8538240fbc..0cef10c2f4a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -1096,6 +1096,34 @@ public extension TelegramEngine.EngineData.Item { } } + public struct PaidMediaAllowed: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Bool + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedChannelData { + return cachedData.flags.contains(.paidMediaAllowed) + } else { + return false + } + } + } + public struct BoostsToUnrestrict: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public typealias Result = Int32? diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index 380047db068..cdbcd868d8c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -16,16 +16,12 @@ public enum RequestSimpleWebViewSource { case settings } -public enum RequestSimpleWebViewError { - case generic -} - -func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: PeerId, url: String?, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { +func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: PeerId, url: String?, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { var serializedThemeParams: Api.DataJSON? if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) { serializedThemeParams = .dataJSON(data: dataString) } - return postbox.transaction { transaction -> Signal in + return postbox.transaction { transaction -> Signal in guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else { return .fail(.generic) } @@ -46,17 +42,21 @@ func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: P flags |= (1 << 3) } return network.request(Api.functions.messages.requestSimpleWebView(flags: flags, bot: inputUser, url: url, startParam: nil, themeParams: serializedThemeParams, platform: botWebViewPlatform)) - |> mapError { _ -> RequestSimpleWebViewError in + |> mapError { _ -> RequestWebViewError in return .generic } - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal in switch result { - case let .simpleWebViewResultUrl(url): - return .single(url) + case let .webViewResultUrl(flags, queryId, url): + var resultFlags: RequestWebViewResult.Flags = [] + if (flags & (1 << 1)) != 0 { + resultFlags.insert(.fullSize) + } + return .single(RequestWebViewResult(flags: resultFlags, queryId: queryId, url: url, keepAliveSignal: nil)) } } } - |> castError(RequestSimpleWebViewError.self) + |> castError(RequestWebViewError.self) |> switchToLatest } @@ -65,9 +65,24 @@ public enum KeepWebViewError { } public struct RequestWebViewResult { - public let queryId: Int64 + public struct Flags: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public init() { + self.rawValue = 0 + } + + public static let fullSize = Flags(rawValue: 1 << 0) + } + + public let flags: Flags + public let queryId: Int64? public let url: String - public let keepAliveSignal: Signal + public let keepAliveSignal: Signal? } public enum RequestWebViewError { @@ -166,8 +181,19 @@ func _internal_requestWebView(postbox: Postbox, network: Network, stateManager: } |> mapToSignal { result -> Signal in switch result { - case let .webViewResultUrl(queryId, url): - return .single(RequestWebViewResult(queryId: queryId, url: url, keepAliveSignal: keepWebViewSignal(network: network, stateManager: stateManager, flags: flags, peer: inputPeer, bot: inputBot, queryId: queryId, replyToMessageId: replyToMessageId, threadId: threadId, sendAs: nil))) + case let .webViewResultUrl(webViewFlags, queryId, url): + var resultFlags: RequestWebViewResult.Flags = [] + if (webViewFlags & (1 << 1)) != 0 { + resultFlags.insert(.fullSize) + } + let keepAlive: Signal? + if let queryId { + keepAlive = keepWebViewSignal(network: network, stateManager: stateManager, flags: flags, peer: inputPeer, bot: inputBot, queryId: queryId, replyToMessageId: replyToMessageId, threadId: threadId, sendAs: nil) + } else { + keepAlive = nil + } + + return .single(RequestWebViewResult(flags: resultFlags, queryId: queryId, url: url, keepAliveSignal: keepAlive)) } } } @@ -199,17 +225,13 @@ func _internal_sendWebViewData(postbox: Postbox, network: Network, stateManager: |> switchToLatest } -public enum RequestAppWebViewError { - case generic -} - -func _internal_requestAppWebView(postbox: Postbox, network: Network, stateManager: AccountStateManager, peerId: PeerId, appReference: BotAppReference, payload: String?, themeParams: [String: Any]?, allowWrite: Bool) -> Signal { +func _internal_requestAppWebView(postbox: Postbox, network: Network, stateManager: AccountStateManager, peerId: PeerId, appReference: BotAppReference, payload: String?, themeParams: [String: Any]?, compact: Bool, allowWrite: Bool) -> Signal { var serializedThemeParams: Api.DataJSON? if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) { serializedThemeParams = .dataJSON(data: dataString) } - return postbox.transaction { transaction -> Signal in + return postbox.transaction { transaction -> Signal in guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { return .fail(.generic) } @@ -235,19 +257,26 @@ func _internal_requestAppWebView(postbox: Postbox, network: Network, stateManage if allowWrite { flags |= (1 << 0) } + if compact { + flags |= (1 << 7) + } return network.request(Api.functions.messages.requestAppWebView(flags: flags, peer: inputPeer, app: app, startParam: payload, themeParams: serializedThemeParams, platform: botWebViewPlatform)) - |> mapError { _ -> RequestAppWebViewError in + |> mapError { _ -> RequestWebViewError in return .generic } - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal in switch result { - case let .appWebViewResultUrl(url): - return .single(url) + case let .webViewResultUrl(flags, queryId, url): + var resultFlags: RequestWebViewResult.Flags = [] + if (flags & (1 << 1)) != 0 { + resultFlags.insert(.fullSize) + } + return .single(RequestWebViewResult(flags: resultFlags, queryId: queryId, url: url, keepAliveSignal: nil)) } } } - |> castError(RequestAppWebViewError.self) + |> castError(RequestWebViewError.self) |> switchToLatest } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift index 5a4955128cd..558e7408b9d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift @@ -50,7 +50,7 @@ func _internal_clearCloudDraftsInteractively(postbox: Postbox, network: Network, innerFlags |= 1 << 0 replyTo = .inputReplyToMessage(flags: innerFlags, replyToMsgId: 0, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil) } - signals.append(network.request(Api.functions.messages.saveDraft(flags: flags, replyTo: replyTo, peer: inputPeer, message: "", entities: nil, media: nil)) + signals.append(network.request(Api.functions.messages.saveDraft(flags: flags, replyTo: replyTo, peer: inputPeer, message: "", entities: nil, media: nil, effect: nil)) |> `catch` { _ -> Signal in return .single(.boolFalse) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ExtendedMedia.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ExtendedMedia.swift new file mode 100644 index 00000000000..f45bf908066 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ExtendedMedia.swift @@ -0,0 +1,40 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +private func _internal_updateExtendedMediaById(account: Account, peerId: EnginePeer.Id, messageIds: [EngineMessage.Id]) -> Signal { + return account.postbox.transaction { transaction -> Peer? in + if let peer = transaction.getPeer(peerId) { + return peer + } else { + return nil + } + } + |> mapToSignal { peer -> Signal in + guard let peer = peer, let inputPeer = apiInputPeer(peer) else { + return .complete() + } + return account.network.request(Api.functions.messages.getExtendedMedia(peer: inputPeer, id: messageIds.map { $0.id })) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates = updates { + account.stateManager.addUpdates(updates) + } + return .complete() + } + } +} + +func _internal_updateExtendedMedia(account: Account, messageIds: [EngineMessage.Id]) -> Signal { + var signals: [Signal] = [] + for (peerId, messageIds) in messagesIdsGroupedByPeerId(messageIds) { + signals.append(_internal_updateExtendedMediaById(account: account, peerId: peerId, messageIds: messageIds)) + } + return combineLatest(signals) + |> ignoreValues +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift index 8d5044b6dcf..e841185a88e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift @@ -19,6 +19,7 @@ public enum EngineMedia: Equatable { case story(TelegramMediaStory) case giveaway(TelegramMediaGiveaway) case giveawayResults(TelegramMediaGiveawayResults) + case paidContent(TelegramMediaPaidContent) } public extension EngineMedia { @@ -56,6 +57,8 @@ public extension EngineMedia { return giveaway.id case let .giveawayResults(giveawayResults): return giveawayResults.id + case let .paidContent(paidContent): + return paidContent.id } } } @@ -95,6 +98,8 @@ public extension EngineMedia { self = .giveaway(giveaway) case let giveawayResults as TelegramMediaGiveawayResults: self = .giveawayResults(giveawayResults) + case let paidContent as TelegramMediaPaidContent: + self = .paidContent(paidContent) default: preconditionFailure() } @@ -134,6 +139,8 @@ public extension EngineMedia { return giveaway case let .giveawayResults(giveawayResults): return giveawayResults + case let .paidContent(paidContent): + return paidContent } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift index 7f810cccacc..74d9ae7a78d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift @@ -16,6 +16,7 @@ public enum MediaArea: Codable, Equatable { case width case height case rotation + case cornerRadius } public var x: Double @@ -23,19 +24,22 @@ public enum MediaArea: Codable, Equatable { public var width: Double public var height: Double public var rotation: Double + public var cornerRadius: Double? public init( x: Double, y: Double, width: Double, height: Double, - rotation: Double + rotation: Double, + cornerRadius: Double? ) { self.x = x self.y = y self.width = width self.height = height self.rotation = rotation + self.cornerRadius = cornerRadius } public init(from decoder: Decoder) throws { @@ -46,6 +50,7 @@ public enum MediaArea: Codable, Equatable { self.width = try container.decode(Double.self, forKey: .width) self.height = try container.decode(Double.self, forKey: .height) self.rotation = try container.decode(Double.self, forKey: .rotation) + self.cornerRadius = try container.decodeIfPresent(Double.self, forKey: .cornerRadius) } public func encode(to encoder: Encoder) throws { @@ -56,6 +61,7 @@ public enum MediaArea: Codable, Equatable { try container.encode(self.width, forKey: .width) try container.encode(self.height, forKey: .height) try container.encode(self.rotation, forKey: .rotation) + try container.encodeIfPresent(self.cornerRadius, forKey: .cornerRadius) } } @@ -64,6 +70,7 @@ public enum MediaArea: Codable, Equatable { case latitude case longitude case venue + case address case queryId case resultId } @@ -71,6 +78,7 @@ public enum MediaArea: Codable, Equatable { public let latitude: Double public let longitude: Double public let venue: MapVenue? + public let address: MapGeoAddress? public let queryId: Int64? public let resultId: String? @@ -78,12 +86,14 @@ public enum MediaArea: Codable, Equatable { latitude: Double, longitude: Double, venue: MapVenue?, + address: MapGeoAddress?, queryId: Int64?, resultId: String? ) { self.latitude = latitude self.longitude = longitude self.venue = venue + self.address = address self.queryId = queryId self.resultId = resultId } @@ -100,6 +110,12 @@ public enum MediaArea: Codable, Equatable { self.venue = nil } + if let addressData = try container.decodeIfPresent(Data.self, forKey: .address) { + self.address = PostboxDecoder(buffer: MemoryBuffer(data: addressData)).decodeRootObject() as? MapGeoAddress + } else { + self.address = nil + } + self.queryId = try container.decodeIfPresent(Int64.self, forKey: .queryId) self.resultId = try container.decodeIfPresent(String.self, forKey: .resultId) } @@ -117,6 +133,13 @@ public enum MediaArea: Codable, Equatable { try container.encode(venueData, forKey: .venue) } + if let address = self.address { + let encoder = PostboxEncoder() + encoder.encodeRootObject(address) + let addressData = encoder.makeData() + try container.encode(addressData, forKey: .address) + } + try container.encodeIfPresent(self.queryId, forKey: .queryId) try container.encodeIfPresent(self.resultId, forKey: .resultId) } @@ -125,7 +148,8 @@ public enum MediaArea: Codable, Equatable { case venue(coordinates: Coordinates, venue: Venue) case reaction(coordinates: Coordinates, reaction: MessageReaction.Reaction, flags: ReactionFlags) case channelMessage(coordinates: Coordinates, messageId: EngineMessage.Id) - + case link(coordinates: Coordinates, url: String) + public struct ReactionFlags: OptionSet { public var rawValue: Int32 @@ -146,6 +170,7 @@ public enum MediaArea: Codable, Equatable { case venue case reaction case channelMessage + case link } public enum DecodingError: Error { @@ -172,6 +197,10 @@ public enum MediaArea: Codable, Equatable { let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates) let messageId = try container.decode(MessageId.self, forKey: .value) self = .channelMessage(coordinates: coordinates, messageId: messageId) + case .link: + let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates) + let url = try container.decode(String.self, forKey: .value) + self = .link(coordinates: coordinates, url: url) } } @@ -192,6 +221,10 @@ public enum MediaArea: Codable, Equatable { try container.encode(MediaAreaType.channelMessage.rawValue, forKey: .type) try container.encode(coordinates, forKey: .coordinates) try container.encode(messageId, forKey: .value) + case let .link(coordinates, url): + try container.encode(MediaAreaType.link.rawValue, forKey: .type) + try container.encode(coordinates, forKey: .coordinates) + try container.encode(url, forKey: .value) } } } @@ -205,6 +238,8 @@ public extension MediaArea { return coordinates case let .channelMessage(coordinates, _): return coordinates + case let .link(coordinates, _): + return coordinates } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift index 2f5b83d23f8..989ff1ddd8d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift @@ -212,7 +212,7 @@ func _internal_shortcutMessageList(account: Account, onlyRemote: Bool) -> Signal if onlyRemote { pendingShortcuts = .single([:]) } else { - pendingShortcuts = account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: account.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 100, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .just(Set([Namespaces.Message.QuickReplyLocal])), orderStatistics: []) + pendingShortcuts = account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: account.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: 100, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .just(Set([Namespaces.Message.QuickReplyLocal])), orderStatistics: []) |> map { view , _, _ -> [String: EngineMessage] in var topMessages: [String: EngineMessage] = [:] for entry in view.entries { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift index 4c57ab1f131..2d024612326 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift @@ -840,6 +840,7 @@ func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: Messa maxReadOutgoingMessageId: nil )), ignoreMessagesInTimestampRange: nil, + ignoreMessageIds: Set(), count: 40, clipHoles: true, anchor: inputAnchor, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift index f6f258c1546..9aa4fff9e3f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift @@ -192,7 +192,7 @@ public final class SparseMessageList { let location: ChatLocationInput = .peer(peerId: self.peerId, threadId: self.threadId) - self.topItemsDisposable.set((self.account.postbox.aroundMessageHistoryViewForLocation(location, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: count, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: .tag(self.messageTag), appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: []) + self.topItemsDisposable.set((self.account.postbox.aroundMessageHistoryViewForLocation(location, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: count, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: .tag(self.messageTag), appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: []) |> deliverOn(self.queue)).start(next: { [weak self] view, updateType, _ in guard let strongSelf = self else { return diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 601f6d15d79..38c07f640bc 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1093,7 +1093,7 @@ func _internal_uploadStoryImpl( |> mapToSignal { result -> Signal in switch result { case let .progress(progress): - return .single(.progress(progress)) + return .single(.progress(progress.progress)) case let .content(content): return postbox.transaction { transaction -> Signal in let privacyRules = apiInputPrivacyRules(privacy: privacy, transaction: transaction) @@ -1278,7 +1278,7 @@ func _internal_editStory(account: Account, peerId: PeerId, id: Int32, media: Eng return contentSignal |> mapToSignal { result -> Signal in if let result = result, case let .progress(progress) = result { - return .single(.progress(progress)) + return .single(.progress(progress.progress)) } let inputMedia: Api.InputMedia? @@ -2474,11 +2474,14 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int return (updatedItemValue, inputPeer) } |> mapToSignal { storyItem, inputPeer -> Signal in - guard let storyItem = storyItem, let inputPeer = inputPeer else { + guard let inputPeer = inputPeer else { return .complete() } - account.stateManager.injectStoryUpdates(updates: [InternalStoryUpdate.added(peerId: peerId, item: storyItem)]) + if let storyItem { + account.stateManager.injectStoryUpdates(updates: [InternalStoryUpdate.added(peerId: peerId, item: storyItem)]) + } + account.stateManager.injectStoryUpdates(updates: [InternalStoryUpdate.updateMyReaction(peerId: peerId, id: id, reaction: reaction)]) return account.network.request(Api.functions.stories.sendReaction(flags: 0, peer: inputPeer, storyId: id, reaction: reaction?.apiReaction ?? .reactionEmpty)) |> map(Optional.init) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 7346f5fa529..4357f346e41 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -9,6 +9,7 @@ enum InternalStoryUpdate { case added(peerId: PeerId, item: Stories.StoredItem) case read(peerId: PeerId, maxId: Int32) case updatePinnedToTopList(peerId: PeerId, ids: [Int32]) + case updateMyReaction(peerId: PeerId, id: Int32, reaction: MessageReaction.Reaction?) } public final class EngineStoryItem: Equatable { @@ -533,7 +534,76 @@ private final class CachedPeerStoryListHead: Codable { } } -public final class PeerStoryListContext { +public struct StoryListContextState: Equatable { + public final class Item: Equatable { + public let id: StoryId + public let storyItem: EngineStoryItem + public let peer: EnginePeer? + + public init(id: StoryId, storyItem: EngineStoryItem, peer: EnginePeer?) { + self.id = id + self.storyItem = storyItem + self.peer = peer + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs === rhs { + return true + } + if lhs.id != rhs.id { + return false + } + if lhs.storyItem != rhs.storyItem { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + } + + public var peerReference: PeerReference? + public var items: [Item] + public var pinnedIds: Set + public var totalCount: Int + public var loadMoreToken: AnyHashable? + public var isCached: Bool + public var hasCache: Bool + public var allEntityFiles: [MediaId: TelegramMediaFile] + public var isLoading: Bool + public init( + peerReference: PeerReference?, + items: [Item], + pinnedIds: Set, + totalCount: Int, + loadMoreToken: AnyHashable?, + isCached: Bool, + hasCache: Bool, + allEntityFiles: [MediaId: TelegramMediaFile], + isLoading: Bool + ) { + self.peerReference = peerReference + self.items = items + self.pinnedIds = pinnedIds + self.totalCount = totalCount + self.loadMoreToken = loadMoreToken + self.isCached = isCached + self.hasCache = hasCache + self.allEntityFiles = allEntityFiles + self.isLoading = isLoading + } +} + +public protocol StoryListContext: AnyObject { + typealias State = StoryListContextState + + var state: Signal { get } + + func loadMore(completion: (() -> Void)?) +} + +public final class PeerStoryListContext: StoryListContext { private final class Impl { private let queue: Queue private let account: Account @@ -555,7 +625,7 @@ public final class PeerStoryListContext { private var updatesDisposable: Disposable? - private var completionCallbacksByToken: [Int: [() -> Void]] = [:] + private var completionCallbacksByToken: [AnyHashable: [() -> Void]] = [:] init(queue: Queue, account: Account, peerId: EnginePeer.Id, isArchived: Bool) { self.queue = queue @@ -563,9 +633,9 @@ public final class PeerStoryListContext { self.peerId = peerId self.isArchived = isArchived - self.stateValue = State(peerReference: nil, items: [], pinnedIds: Set(), totalCount: 0, loadMoreToken: 0, isCached: true, hasCache: false, allEntityFiles: [:]) + self.stateValue = State(peerReference: nil, items: [], pinnedIds: Set(), totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false) - let _ = (account.postbox.transaction { transaction -> (PeerReference?, [EngineStoryItem], [Int32], Int, [MediaId: TelegramMediaFile], Bool) in + let _ = (account.postbox.transaction { transaction -> (PeerReference?, [State.Item], [Int32], Int, [MediaId: TelegramMediaFile], Bool) in let key = ValueBoxKey(length: 8 + 1) key.setInt64(0, value: peerId.toInt64()) key.setInt8(8, value: isArchived ? 1 : 0) @@ -573,7 +643,7 @@ public final class PeerStoryListContext { guard let cached = cached else { return (nil, [], [], 0, [:], false) } - var items: [EngineStoryItem] = [] + var items: [State.Item] = [] var allEntityFiles: [MediaId: TelegramMediaFile] = [:] for storedItem in cached.items { if case let .item(item) = storedItem, let media = item.media { @@ -613,7 +683,11 @@ public final class PeerStoryListContext { forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) } ) - items.append(mappedItem) + items.append(State.Item( + id: StoryId(peerId: peerId, id: mappedItem.id), + storyItem: mappedItem, + peer: nil + )) for entity in mappedItem.entities { if case let .CustomEmoji(_, fileId) = entity.type { @@ -649,10 +723,10 @@ public final class PeerStoryListContext { return } - var updatedState = State(peerReference: peerReference, items: items, pinnedIds: Set(pinnedIds), totalCount: totalCount, loadMoreToken: 0, isCached: true, hasCache: hasCache, allEntityFiles: allEntityFiles) + var updatedState = State(peerReference: peerReference, items: items, pinnedIds: Set(pinnedIds), totalCount: totalCount, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: hasCache, allEntityFiles: allEntityFiles, isLoading: false) updatedState.items.sort(by: { lhs, rhs in - let lhsPinned = updatedState.pinnedIds.contains(lhs.id) - let rhsPinned = updatedState.pinnedIds.contains(rhs.id) + let lhsPinned = updatedState.pinnedIds.contains(lhs.storyItem.id) + let rhsPinned = updatedState.pinnedIds.contains(rhs.storyItem.id) if lhsPinned != rhsPinned { if lhsPinned { return true @@ -660,7 +734,7 @@ public final class PeerStoryListContext { return false } } - return lhs.timestamp > rhs.timestamp + return lhs.storyItem.timestamp > rhs.storyItem.timestamp }) self.stateValue = updatedState @@ -673,7 +747,7 @@ public final class PeerStoryListContext { } func loadMore(completion: (() -> Void)?) { - guard let loadMoreToken = self.stateValue.loadMoreToken else { + guard let loadMoreTokenValue = self.stateValue.loadMoreToken, let loadMoreToken = loadMoreTokenValue.base as? Int else { return } @@ -699,7 +773,7 @@ public final class PeerStoryListContext { self.requestDisposable = (self.account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) } - |> mapToSignal { inputPeer -> Signal<([EngineStoryItem], Int, PeerReference?, Bool), NoError> in + |> mapToSignal { inputPeer -> Signal<([State.Item], Int, PeerReference?, Bool), NoError> in guard let inputPeer = inputPeer else { return .single(([], 0, nil, false)) } @@ -717,13 +791,13 @@ public final class PeerStoryListContext { |> `catch` { _ -> Signal in return .single(nil) } - |> mapToSignal { result -> Signal<([EngineStoryItem], Int, PeerReference?, Bool), NoError> in + |> mapToSignal { result -> Signal<([State.Item], Int, PeerReference?, Bool), NoError> in guard let result = result else { return .single(([], 0, nil, false)) } - return account.postbox.transaction { transaction -> ([EngineStoryItem], Int, PeerReference?, Bool) in - var storyItems: [EngineStoryItem] = [] + return account.postbox.transaction { transaction -> ([State.Item], Int, PeerReference?, Bool) in + var storyItems: [State.Item] = [] var totalCount: Int = 0 var hasMore: Bool = false @@ -775,7 +849,11 @@ public final class PeerStoryListContext { forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) } ) - storyItems.append(mappedItem) + storyItems.append(State.Item( + id: StoryId(peerId: peerId, id: mappedItem.id), + storyItem: mappedItem, + peer: nil + )) } } } @@ -784,7 +862,7 @@ public final class PeerStoryListContext { let key = ValueBoxKey(length: 8 + 1) key.setInt64(0, value: peerId.toInt64()) key.setInt8(8, value: isArchived ? 1 : 0) - if let entry = CodableEntry(CachedPeerStoryListHead(items: storyItems.prefix(100).map { .item($0.asStoryItem()) }, pinnedIds: Array(pinnedIds), totalCount: count)) { + if let entry = CodableEntry(CachedPeerStoryListHead(items: storyItems.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: Array(pinnedIds), totalCount: count)) { transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) } } @@ -795,7 +873,7 @@ public final class PeerStoryListContext { } } |> deliverOn(self.queue)).start(next: { [weak self] storyItems, totalCount, peerReference, hasMore in - guard let `self` = self else { + guard let self else { return } @@ -808,12 +886,12 @@ public final class PeerStoryListContext { } updatedState.hasCache = true - var existingIds = Set(updatedState.items.map { $0.id }) + var existingIds = Set(updatedState.items.map { $0.storyItem.id }) for item in storyItems { - if existingIds.contains(item.id) { + if existingIds.contains(item.storyItem.id) { continue } - existingIds.insert(item.id) + existingIds.insert(item.storyItem.id) updatedState.items.append(item) } @@ -823,7 +901,7 @@ public final class PeerStoryListContext { } if hasMore { - updatedState.loadMoreToken = (storyItems.last?.id).flatMap(Int.init) + updatedState.loadMoreToken = (storyItems.last?.storyItem.id).flatMap(Int.init).flatMap({ AnyHashable($0) }) } else { updatedState.loadMoreToken = nil } @@ -834,7 +912,7 @@ public final class PeerStoryListContext { } self.stateValue = updatedState - if let callbacks = self.completionCallbacksByToken.removeValue(forKey: loadMoreToken) { + if let callbacks = self.completionCallbacksByToken.removeValue(forKey: AnyHashable(loadMoreToken)) { for f in callbacks { f() } @@ -882,17 +960,19 @@ public final class PeerStoryListContext { return peers } |> deliverOn(self.queue)).start(next: { [weak self] peers in - guard let `self` = self else { + guard let self else { return } var finalUpdatedState: State? + finalUpdatedState = nil + let _ = finalUpdatedState for update in updates { switch update { case let .deleted(peerId, id): if self.peerId == peerId { - if let index = (finalUpdatedState ?? self.stateValue).items.firstIndex(where: { $0.id == id }) { + if let index = (finalUpdatedState ?? self.stateValue).items.firstIndex(where: { $0.storyItem.id == id }) { var updatedState = finalUpdatedState ?? self.stateValue updatedState.items.remove(at: index) updatedState.totalCount = max(0, updatedState.totalCount - 1) @@ -901,13 +981,13 @@ public final class PeerStoryListContext { } case let .added(peerId, item): if self.peerId == peerId { - if let index = (finalUpdatedState ?? self.stateValue).items.firstIndex(where: { $0.id == item.id }) { + if let index = (finalUpdatedState ?? self.stateValue).items.firstIndex(where: { $0.storyItem.id == item.id }) { if !self.isArchived { if case let .item(item) = item { if item.isPinned { if let media = item.media { var updatedState = finalUpdatedState ?? self.stateValue - updatedState.items[index] = EngineStoryItem( + updatedState.items[index] = State.Item(id: StoryId(peerId: peerId, id: item.id), storyItem: EngineStoryItem( id: item.id, timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, @@ -942,7 +1022,7 @@ public final class PeerStoryListContext { myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }, author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } - ) + ), peer: nil) finalUpdatedState = updatedState } } else { @@ -956,7 +1036,7 @@ public final class PeerStoryListContext { if case let .item(item) = item { if let media = item.media { var updatedState = finalUpdatedState ?? self.stateValue - updatedState.items[index] = EngineStoryItem( + updatedState.items[index] = State.Item(id: StoryId(peerId: peerId, id: item.id), storyItem: EngineStoryItem( id: item.id, timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, @@ -991,7 +1071,7 @@ public final class PeerStoryListContext { myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }, author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } - ) + ), peer: nil) finalUpdatedState = updatedState } else { var updatedState = finalUpdatedState ?? self.stateValue @@ -1007,7 +1087,7 @@ public final class PeerStoryListContext { if item.isPinned { if let media = item.media { var updatedState = finalUpdatedState ?? self.stateValue - updatedState.items.append(EngineStoryItem( + updatedState.items.append(State.Item(id: StoryId(peerId: peerId, id: item.id), storyItem: EngineStoryItem( id: item.id, timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, @@ -1042,10 +1122,10 @@ public final class PeerStoryListContext { myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }, author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } - )) + ), peer: nil)) updatedState.items.sort(by: { lhs, rhs in - let lhsPinned = updatedState.pinnedIds.contains(lhs.id) - let rhsPinned = updatedState.pinnedIds.contains(rhs.id) + let lhsPinned = updatedState.pinnedIds.contains(lhs.storyItem.id) + let rhsPinned = updatedState.pinnedIds.contains(rhs.storyItem.id) if lhsPinned != rhsPinned { if lhsPinned { return true @@ -1053,7 +1133,7 @@ public final class PeerStoryListContext { return false } } - return lhs.timestamp > rhs.timestamp + return lhs.storyItem.timestamp > rhs.storyItem.timestamp }) finalUpdatedState = updatedState } @@ -1063,7 +1143,7 @@ public final class PeerStoryListContext { if case let .item(item) = item { if let media = item.media { var updatedState = finalUpdatedState ?? self.stateValue - updatedState.items.append(EngineStoryItem( + updatedState.items.append(State.Item(id: StoryId(peerId: peerId, id: item.id), storyItem: EngineStoryItem( id: item.id, timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, @@ -1098,10 +1178,10 @@ public final class PeerStoryListContext { myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }, author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } - )) + ), peer: nil)) updatedState.items.sort(by: { lhs, rhs in - let lhsPinned = updatedState.pinnedIds.contains(lhs.id) - let rhsPinned = updatedState.pinnedIds.contains(rhs.id) + let lhsPinned = updatedState.pinnedIds.contains(lhs.storyItem.id) + let rhsPinned = updatedState.pinnedIds.contains(rhs.storyItem.id) if lhsPinned != rhsPinned { if lhsPinned { return true @@ -1109,7 +1189,7 @@ public final class PeerStoryListContext { return false } } - return lhs.timestamp > rhs.timestamp + return lhs.storyItem.timestamp > rhs.storyItem.timestamp }) finalUpdatedState = updatedState } @@ -1119,6 +1199,8 @@ public final class PeerStoryListContext { } case .read: break + case .updateMyReaction: + break case let .updatePinnedToTopList(peerId, ids): if self.peerId == peerId && !self.isArchived { let previousIds = (finalUpdatedState ?? self.stateValue).pinnedIds @@ -1126,8 +1208,8 @@ public final class PeerStoryListContext { var updatedState = finalUpdatedState ?? self.stateValue updatedState.pinnedIds = Set(ids) updatedState.items.sort(by: { lhs, rhs in - let lhsPinned = updatedState.pinnedIds.contains(lhs.id) - let rhsPinned = updatedState.pinnedIds.contains(rhs.id) + let lhsPinned = updatedState.pinnedIds.contains(lhs.storyItem.id) + let rhsPinned = updatedState.pinnedIds.contains(rhs.storyItem.id) if lhsPinned != rhsPinned { if lhsPinned { return true @@ -1135,7 +1217,7 @@ public final class PeerStoryListContext { return false } } - return lhs.timestamp > rhs.timestamp + return lhs.storyItem.timestamp > rhs.storyItem.timestamp }) finalUpdatedState = updatedState } @@ -1153,7 +1235,7 @@ public final class PeerStoryListContext { let key = ValueBoxKey(length: 8 + 1) key.setInt64(0, value: peerId.toInt64()) key.setInt8(8, value: isArchived ? 1 : 0) - if let entry = CodableEntry(CachedPeerStoryListHead(items: items.prefix(100).map { .item($0.asStoryItem()) }, pinnedIds: Array(pinnedIds), totalCount: Int32(totalCount))) { + if let entry = CodableEntry(CachedPeerStoryListHead(items: items.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: Array(pinnedIds), totalCount: Int32(totalCount))) { transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) } }).start() @@ -1165,34 +1247,410 @@ public final class PeerStoryListContext { } } - public struct State: Equatable { - public var peerReference: PeerReference? - public var items: [EngineStoryItem] - public var pinnedIds: Set - public var totalCount: Int - public var loadMoreToken: Int? - public var isCached: Bool - public var hasCache: Bool - public var allEntityFiles: [MediaId: TelegramMediaFile] + public var state: Signal { + return impl.signalWith { impl, subscriber in + return impl.state.start(next: subscriber.putNext) + } + } + + private let queue: Queue + private let impl: QueueLocalObject + + public init(account: Account, peerId: EnginePeer.Id, isArchived: Bool) { + let queue = Queue.mainQueue() + self.queue = queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, account: account, peerId: peerId, isArchived: isArchived) + }) + } + + public func loadMore(completion: (() -> Void)? = nil) { + self.impl.with { impl in + impl.loadMore(completion : completion) + } + } +} + +public final class SearchStoryListContext: StoryListContext { + public enum Source { + case hashtag(String) + case mediaArea(MediaArea) + } + + private final class Impl { + private let queue: Queue + private let account: Account + private let source: Source - public init( - peerReference: PeerReference?, - items: [EngineStoryItem], - pinnedIds: Set, - totalCount: Int, - loadMoreToken: Int?, - isCached: Bool, - hasCache: Bool, - allEntityFiles: [MediaId: TelegramMediaFile] - ) { - self.peerReference = peerReference - self.items = items - self.pinnedIds = pinnedIds - self.totalCount = totalCount - self.loadMoreToken = loadMoreToken - self.isCached = isCached - self.hasCache = hasCache - self.allEntityFiles = allEntityFiles + private let statePromise = Promise() + private var stateValue: State { + didSet { + self.statePromise.set(.single(self.stateValue)) + } + } + var state: Signal { + return self.statePromise.get() + } + + private var isLoadingMore: Bool = false { + didSet { + self.stateValue.isLoading = isLoadingMore + } + } + private var requestDisposable: Disposable? + + private var updatesDisposable: Disposable? + + private var completionCallbacksByToken: [AnyHashable: [() -> Void]] = [:] + + init(queue: Queue, account: Account, source: Source) { + self.queue = queue + self.account = account + self.source = source + + self.stateValue = State(peerReference: nil, items: [], pinnedIds: Set(), totalCount: 0, loadMoreToken: AnyHashable(""), isCached: false, hasCache: false, allEntityFiles: [:], isLoading: false) + self.statePromise.set(.single(self.stateValue)) + + self.loadMore(completion: nil) + } + + deinit { + self.requestDisposable?.dispose() + } + + func loadMore(completion: (() -> Void)?) { + guard let loadMoreTokenValue = self.stateValue.loadMoreToken, let loadMoreToken = loadMoreTokenValue.base as? String else { + return + } + + if let completion = completion { + if self.completionCallbacksByToken[loadMoreToken] == nil { + self.completionCallbacksByToken[loadMoreToken] = [] + } + self.completionCallbacksByToken[loadMoreToken]?.append(completion) + } + + if self.isLoadingMore { + return + } + + self.isLoadingMore = true + + let limit = 100 + + let account = self.account + let accountPeerId = account.peerId + + var searchHashtag: String? = nil + var area: Api.MediaArea? = nil + + var flags: Int32 = 0 + switch source { + case let .hashtag(query): + if query.hasPrefix("#") { + searchHashtag = String(query[query.index(after: query.startIndex)...]) + } else { + searchHashtag = query + } + flags |= (1 << 0) + case let .mediaArea(mediaArea): + area = apiMediaAreasFromMediaAreas([mediaArea], transaction: nil).first + flags |= (1 << 1) + } + + self.requestDisposable = (account.network.request(Api.functions.stories.searchPosts(flags: flags, hashtag: searchHashtag, area: area, offset: loadMoreToken, limit: Int32(limit))) + |> map { result -> Api.stories.FoundStories? in + return result + } + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<([State.Item], Int, String?), NoError> in + guard let result else { + return .single(([], 0, nil)) + } + + return account.postbox.transaction { transaction -> ([State.Item], Int, String?) in + var storyItems: [State.Item] = [] + var totalCount: Int = 0 + var nextOffsetValue: String? + + switch result { + case let .foundStories(_, count, stories, nextOffset, chats, users): + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(transaction: transaction, chats: chats, users: users)) + + totalCount = Int(count) + nextOffsetValue = nextOffset + + for story in stories { + switch story { + case let .foundStory(peer, story): + if let storedItem = Stories.StoredItem(apiStoryItem: story, peerId: peer.peerId, transaction: transaction) { + if case let .item(item) = storedItem, let media = item.media { + let mappedItem = EngineStoryItem( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: EngineMedia(media), + alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + mediaAreas: item.mediaAreas, + text: item.text, + entities: item.entities, + views: item.views.flatMap { views in + return EngineStoryItem.Views( + seenCount: views.seenCount, + reactedCount: views.reactedCount, + forwardCount: views.forwardCount, + seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in + return transaction.getPeer(id).flatMap(EnginePeer.init) + }, + reactions: views.reactions, + hasList: views.hasList + ) + }, + privacy: item.privacy.flatMap(EngineStoryPrivacy.init), + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic, + isPending: false, + isCloseFriends: item.isCloseFriends, + isContacts: item.isContacts, + isSelectedContacts: item.isSelectedContacts, + isForwardingDisabled: item.isForwardingDisabled, + isEdited: item.isEdited, + isMy: item.isMy, + myReaction: item.myReaction, + forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, + author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) } + ) + storyItems.append(State.Item( + id: StoryId(peerId: peer.peerId, id: mappedItem.id), + storyItem: mappedItem, + peer: transaction.getPeer(peer.peerId).flatMap(EnginePeer.init) + )) + } + } + } + } + } + + return (storyItems, totalCount, nextOffsetValue) + } + } + |> deliverOn(self.queue)).start(next: { [weak self] storyItems, totalCount, nextOffset in + guard let `self` = self else { + return + } + + self.isLoadingMore = false + + var updatedState = self.stateValue + updatedState.hasCache = true + + var existingIds = Set(updatedState.items.map { $0.id }) + for item in storyItems { + if existingIds.contains(item.id) { + continue + } + existingIds.insert(item.id) + + updatedState.items.append(item) + } + + if let nextOffset { + updatedState.loadMoreToken = AnyHashable(nextOffset) + } else { + updatedState.loadMoreToken = nil + } + if updatedState.loadMoreToken != nil { + updatedState.totalCount = max(totalCount, updatedState.items.count) + } else { + updatedState.totalCount = updatedState.items.count + } + self.stateValue = updatedState + + if let callbacks = self.completionCallbacksByToken.removeValue(forKey: loadMoreToken) { + for f in callbacks { + f() + } + } + + if self.updatesDisposable == nil { + self.updatesDisposable = (self.account.stateManager.storyUpdates + |> deliverOn(self.queue)).start(next: { [weak self] updates in + guard let self else { + return + } + let _ = (self.account.postbox.transaction { transaction -> [PeerId: Peer] in + var peers: [PeerId: Peer] = [:] + + for update in updates { + switch update { + case let .added(_, item): + if case let .item(item) = item { + if let views = item.views { + for id in views.seenPeerIds { + if let peer = transaction.getPeer(id) { + peers[peer.id] = peer + } + } + } + if let forwardInfo = item.forwardInfo, case let .known(peerId, _, _) = forwardInfo { + if let peer = transaction.getPeer(peerId) { + peers[peer.id] = peer + } + } + if let peerId = item.authorId { + if let peer = transaction.getPeer(peerId) { + peers[peer.id] = peer + } + } + } + case let .updateMyReaction(_, _, reaction): + if reaction != nil { + if let peer = transaction.getPeer(accountPeerId) { + peers[peer.id] = peer + } + } + default: + break + } + } + + return peers + } + |> deliverOn(self.queue)).start(next: { [weak self] peers in + guard let self else { + return + } + + var finalUpdatedState: State? + for update in updates { + switch update { + case .deleted: + break + case let .added(peerId, item): + if let index = (finalUpdatedState ?? self.stateValue).items.firstIndex(where: { $0.id == StoryId(peerId: peerId, id: item.id) }) { + let currentItem = (finalUpdatedState ?? self.stateValue).items[index] + if case let .item(item) = item, let media = item.media { + var updatedState = finalUpdatedState ?? self.stateValue + updatedState.items[index] = State.Item( + id: StoryId(peerId: peerId, id: item.id), + storyItem: EngineStoryItem( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: EngineMedia(media), + alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + mediaAreas: item.mediaAreas, + text: item.text, + entities: item.entities, + views: item.views.flatMap { views in + return EngineStoryItem.Views( + seenCount: views.seenCount, + reactedCount: views.reactedCount, + forwardCount: views.forwardCount, + seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in + return peers[id].flatMap(EnginePeer.init) + }, + reactions: views.reactions, + hasList: views.hasList + ) + }, + privacy: item.privacy.flatMap(EngineStoryPrivacy.init), + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic, + isPending: false, + isCloseFriends: item.isCloseFriends, + isContacts: item.isContacts, + isSelectedContacts: item.isSelectedContacts, + isForwardingDisabled: item.isForwardingDisabled, + isEdited: item.isEdited, + isMy: item.isMy, + myReaction: item.myReaction, + forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }, + author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } + ), + peer: currentItem.peer + ) + finalUpdatedState = updatedState + } + } + case let .updateMyReaction(peerId, id, reaction): + if let index = (finalUpdatedState ?? self.stateValue).items.firstIndex(where: { $0.id == StoryId(peerId: peerId, id: id) }) { + let item = (finalUpdatedState ?? self.stateValue).items[index] + var updatedState = finalUpdatedState ?? self.stateValue + + let previousViews: Stories.Item.Views? = item.storyItem.views.flatMap { views in + return Stories.Item.Views( + seenCount: views.seenCount, + reactedCount: views.reactedCount, + forwardCount: views.forwardCount, + seenPeerIds: views.seenPeers.map(\.id), + reactions: views.reactions, + hasList: views.hasList + ) + } + let updatedViews = _internal_updateStoryViewsForMyReaction(isChannel: peerId.namespace == Namespaces.Peer.CloudChannel, views: previousViews, previousReaction: item.storyItem.myReaction, reaction: reaction) + let mappedViews = updatedViews.flatMap { views in + return EngineStoryItem.Views( + seenCount: views.seenCount, + reactedCount: views.reactedCount, + forwardCount: views.forwardCount, + seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in + return peers[id].flatMap(EnginePeer.init) + }, + reactions: views.reactions, + hasList: views.hasList + ) + } + + updatedState.items[index] = State.Item( + id: item.id, + storyItem: EngineStoryItem( + id: item.storyItem.id, + timestamp: item.storyItem.timestamp, + expirationTimestamp: item.storyItem.expirationTimestamp, + media: item.storyItem.media, + alternativeMedia: item.storyItem.alternativeMedia, + mediaAreas: item.storyItem.mediaAreas, + text: item.storyItem.text, + entities: item.storyItem.entities, + views: mappedViews, + privacy: item.storyItem.privacy, + isPinned: item.storyItem.isPinned, + isExpired: item.storyItem.isExpired, + isPublic: item.storyItem.isPublic, + isPending: item.storyItem.isPending, + isCloseFriends: item.storyItem.isCloseFriends, + isContacts: item.storyItem.isContacts, + isSelectedContacts: item.storyItem.isSelectedContacts, + isForwardingDisabled: item.storyItem.isForwardingDisabled, + isEdited: item.storyItem.isEdited, + isMy: item.storyItem.isMy, + myReaction: reaction, + forwardInfo: item.storyItem.forwardInfo, + author: item.storyItem.author + ), + peer: item.peer + ) + finalUpdatedState = updatedState + } + case .read: + break + case .updatePinnedToTopList: + break + } + } + + if let finalUpdatedState { + self.stateValue = finalUpdatedState + } + }) + }) + } + }) } } @@ -1205,11 +1663,11 @@ public final class PeerStoryListContext { private let queue: Queue private let impl: QueueLocalObject - public init(account: Account, peerId: EnginePeer.Id, isArchived: Bool) { + public init(account: Account, source: Source) { let queue = Queue.mainQueue() self.queue = queue self.impl = QueueLocalObject(queue: queue, generate: { - return Impl(queue: queue, account: account, peerId: peerId, isArchived: isArchived) + return Impl(queue: queue, account: account, source: source) }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 544704635f1..d2ab82fa6d5 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -410,7 +410,7 @@ public extension TelegramEngine { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) for i in 0 ..< updatedMedia.count { if let media = updatedMedia[i] as? TelegramMediaMap, let _ = media.liveBroadcastingTimeout { - updatedMedia[i] = TelegramMediaMap(latitude: media.latitude, longitude: media.longitude, heading: media.heading, accuracyRadius: media.accuracyRadius, geoPlace: media.geoPlace, venue: media.venue, liveBroadcastingTimeout: max(0, timestamp - currentMessage.timestamp - 1), liveProximityNotificationRadius: nil) + updatedMedia[i] = TelegramMediaMap(latitude: media.latitude, longitude: media.longitude, heading: media.heading, accuracyRadius: media.accuracyRadius, venue: media.venue, liveBroadcastingTimeout: max(0, timestamp - currentMessage.timestamp - 1), liveProximityNotificationRadius: nil) } } return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: updatedMedia)) @@ -580,12 +580,12 @@ public extension TelegramEngine { return _internal_requestWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, botId: botId, url: url, payload: payload, themeParams: themeParams, fromMenu: fromMenu, replyToMessageId: replyToMessageId, threadId: threadId) } - public func requestSimpleWebView(botId: PeerId, url: String?, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { + public func requestSimpleWebView(botId: PeerId, url: String?, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { return _internal_requestSimpleWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, url: url, source: source, themeParams: themeParams) } - public func requestAppWebView(peerId: PeerId, appReference: BotAppReference, payload: String?, themeParams: [String: Any]?, allowWrite: Bool) -> Signal { - return _internal_requestAppWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, appReference: appReference, payload: payload, themeParams: themeParams, allowWrite: allowWrite) + public func requestAppWebView(peerId: PeerId, appReference: BotAppReference, payload: String?, themeParams: [String: Any]?, compact: Bool, allowWrite: Bool) -> Signal { + return _internal_requestAppWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, appReference: appReference, payload: payload, themeParams: themeParams, compact: compact, allowWrite: allowWrite) } public func sendWebViewData(botId: PeerId, buttonText: String, data: String) -> Signal { @@ -1445,6 +1445,10 @@ public extension TelegramEngine { return _internal_reportAdMessage(account: self.account, peerId: peerId, opaqueId: opaqueId, option: option) } + public func updateExtendedMedia(messageIds: [EngineMessage.Id]) -> Signal { + return _internal_updateExtendedMedia(account: self.account, messageIds: messageIds) + } + public func getAllLocalChannels(count: Int) -> Signal<[EnginePeer.Id], NoError> { return self.account.postbox.transaction { transaction -> [EnginePeer.Id] in var result: [EnginePeer.Id] = [] diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 8ab99b612c2..883b9b7351b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -59,6 +59,15 @@ public struct BotPaymentInvoice : Equatable { public let prices: [BotPaymentPrice] public let tip: Tip? public let termsInfo: RecurrentInfo? + + public init(isTest: Bool, requestedFields: BotPaymentInvoiceFields, currency: String, prices: [BotPaymentPrice], tip: Tip?, termsInfo: RecurrentInfo?) { + self.isTest = isTest + self.requestedFields = requestedFields + self.currency = currency + self.prices = prices + self.tip = tip + self.termsInfo = termsInfo + } } public struct BotPaymentNativeProvider : Equatable { @@ -125,6 +134,20 @@ public struct BotPaymentForm : Equatable { public let savedInfo: BotPaymentRequestedInfo? public let savedCredentials: [BotPaymentSavedCredentials] public let additionalPaymentMethods: [BotPaymentMethod] + + public init(id: Int64, canSaveCredentials: Bool, passwordMissing: Bool, invoice: BotPaymentInvoice, paymentBotId: PeerId, providerId: PeerId?, url: String?, nativeProvider: BotPaymentNativeProvider?, savedInfo: BotPaymentRequestedInfo?, savedCredentials: [BotPaymentSavedCredentials], additionalPaymentMethods: [BotPaymentMethod]) { + self.id = id + self.canSaveCredentials = canSaveCredentials + self.passwordMissing = passwordMissing + self.invoice = invoice + self.paymentBotId = paymentBotId + self.providerId = providerId + self.url = url + self.nativeProvider = nativeProvider + self.savedInfo = savedInfo + self.savedCredentials = savedCredentials + self.additionalPaymentMethods = additionalPaymentMethods + } } public struct BotPaymentMethod: Equatable { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 2283d9aae8a..43b9709bc98 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -76,7 +76,7 @@ private enum RequestStarsStateError { case generic } -private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id, subject: StarsTransactionsContext.Subject, offset: String?) -> Signal { +private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id, mode: StarsTransactionsContext.Mode, offset: String?, limit: Int32) -> Signal { return account.postbox.transaction { transaction -> Peer? in return transaction.getPeer(peerId) } @@ -89,7 +89,7 @@ private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id let signal: Signal if let offset { var flags: Int32 = 0 - switch subject { + switch mode { case .incoming: flags = 1 << 0 case .outgoing: @@ -97,7 +97,7 @@ private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id default: break } - signal = account.network.request(Api.functions.payments.getStarsTransactions(flags: flags, peer: inputPeer, offset: offset)) + signal = account.network.request(Api.functions.payments.getStarsTransactions(flags: flags, peer: inputPeer, offset: offset, limit: limit)) } else { signal = account.network.request(Api.functions.payments.getStarsStatus(peer: inputPeer)) } @@ -114,7 +114,7 @@ private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id var parsedTransactions: [StarsContext.State.Transaction] = [] for entry in history { - if let parsedTransaction = StarsContext.State.Transaction(apiTransaction: entry, transaction: transaction) { + if let parsedTransaction = StarsContext.State.Transaction(apiTransaction: entry, peerId: peerId != account.peerId ? peerId : nil, transaction: transaction) { parsedTransactions.append(parsedTransaction) } } @@ -140,11 +140,11 @@ private final class StarsContextImpl { private let disposable = MetaDisposable() private var updateDisposable: Disposable? - init(account: Account, peerId: EnginePeer.Id) { + init(account: Account) { assert(Queue.mainQueue().isCurrent()) self.account = account - self.peerId = peerId + self.peerId = account.peerId self._state = nil self._statePromise.set(.single(nil)) @@ -177,7 +177,7 @@ private final class StarsContextImpl { } self.previousLoadTimestamp = currentTimestamp - self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, subject: .all, offset: nil) + self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, mode: .all, offset: nil, limit: 5) |> deliverOnMainQueue).start(next: { [weak self] status in guard let self else { return @@ -199,7 +199,7 @@ private final class StarsContextImpl { return } var transactions = state.transactions - transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil), at: 0) + transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, media: []), at: 0) self.updateState(StarsContext.State(flags: [.isPendingBalance], balance: state.balance + balance, transactions: transactions, canLoadMore: state.canLoadMore, isLoading: state.isLoading)) } @@ -220,7 +220,7 @@ private final class StarsContextImpl { self._state?.isLoading = true - self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, subject: .all, offset: nextOffset) + self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, mode: .all, offset: nextOffset, limit: 10) |> deliverOnMainQueue).start(next: { [weak self] status in if let self { self.updateState(StarsContext.State(flags: [], balance: status.balance, transactions: currentState.transactions + status.transactions, canLoadMore: status.nextOffset != nil, isLoading: false)) @@ -236,10 +236,11 @@ private final class StarsContextImpl { } private extension StarsContext.State.Transaction { - init?(apiTransaction: Api.StarsTransaction, transaction: Transaction) { + init?(apiTransaction: Api.StarsTransaction, peerId: EnginePeer.Id?, transaction: Transaction) { switch apiTransaction { - case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo): + case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia): let parsedPeer: StarsContext.State.Transaction.Peer + var paidMessageId: MessageId? switch transactionPeer { case .starsTransactionPeerAppStore: parsedPeer = .appStore @@ -249,6 +250,8 @@ private extension StarsContext.State.Transaction { parsedPeer = .fragment case .starsTransactionPeerPremiumBot: parsedPeer = .premiumBot + case .starsTransactionPeerAds: + parsedPeer = .ads case .starsTransactionPeerUnsupported: parsedPeer = .unsupported case let .starsTransactionPeer(apiPeer): @@ -256,13 +259,28 @@ private extension StarsContext.State.Transaction { return nil } parsedPeer = .peer(EnginePeer(peer)) + if let messageId { + if let peerId { + paidMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageId) + } else { + paidMessageId = MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: messageId) + } + } } var flags: Flags = [] if (apiFlags & (1 << 3)) != 0 { flags.insert(.isRefund) } - self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init)) + if (apiFlags & (1 << 4)) != 0 { + flags.insert(.isPending) + } + if (apiFlags & (1 << 6)) != 0 { + flags.insert(.isFailed) + } + + let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? [] + self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, media: media) } } } @@ -279,6 +297,8 @@ public final class StarsContext { public static let isRefund = Flags(rawValue: 1 << 0) public static let isLocal = Flags(rawValue: 1 << 1) + public static let isPending = Flags(rawValue: 1 << 2) + public static let isFailed = Flags(rawValue: 1 << 3) } public enum Peer: Equatable { @@ -286,6 +306,7 @@ public final class StarsContext { case playMarket case fragment case premiumBot + case ads case unsupported case peer(EnginePeer) } @@ -298,6 +319,10 @@ public final class StarsContext { public let title: String? public let description: String? public let photo: TelegramMediaWebFile? + public let transactionDate: Int32? + public let transactionUrl: String? + public let paidMessageId: MessageId? + public let media: [Media] public init( flags: Flags, @@ -307,7 +332,11 @@ public final class StarsContext { peer: Peer, title: String?, description: String?, - photo: TelegramMediaWebFile? + photo: TelegramMediaWebFile?, + transactionDate: Int32?, + transactionUrl: String?, + paidMessageId: MessageId?, + media: [Media] ) { self.flags = flags self.id = id @@ -317,6 +346,50 @@ public final class StarsContext { self.title = title self.description = description self.photo = photo + self.transactionDate = transactionDate + self.transactionUrl = transactionUrl + self.paidMessageId = paidMessageId + self.media = media + } + + public static func == (lhs: Transaction, rhs: Transaction) -> Bool { + if lhs.flags != rhs.flags { + return false + } + if lhs.id != rhs.id { + return false + } + if lhs.count != rhs.count { + return false + } + if lhs.date != rhs.date { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.description != rhs.description { + return false + } + if lhs.photo != rhs.photo { + return false + } + if lhs.transactionDate != rhs.transactionDate { + return false + } + if lhs.transactionUrl != rhs.transactionUrl { + return false + } + if lhs.paidMessageId != rhs.paidMessageId { + return false + } + if !areMediaArraysEqual(lhs.media, rhs.media) { + return false + } + return true } } @@ -419,9 +492,9 @@ public final class StarsContext { } } - init(account: Account, peerId: EnginePeer.Id) { + init(account: Account) { self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { - return StarsContextImpl(account: account, peerId: peerId) + return StarsContextImpl(account: account) }) } } @@ -430,7 +503,7 @@ private final class StarsTransactionsContextImpl { private let account: Account private weak var starsContext: StarsContext? private let peerId: EnginePeer.Id - private let subject: StarsTransactionsContext.Subject + private let mode: StarsTransactionsContext.Mode private var _state: StarsTransactionsContext.State private let _statePromise = Promise() @@ -442,17 +515,22 @@ private final class StarsTransactionsContextImpl { private let disposable = MetaDisposable() private var stateDisposable: Disposable? - init(account: Account, starsContext: StarsContext, subject: StarsTransactionsContext.Subject) { + init(account: Account, subject: StarsTransactionsContext.Subject, mode: StarsTransactionsContext.Mode) { assert(Queue.mainQueue().isCurrent()) self.account = account - self.starsContext = starsContext - self.peerId = starsContext.peerId - self.subject = subject + switch subject { + case let .starsContext(starsContext): + self.starsContext = starsContext + self.peerId = starsContext.peerId + case let .peer(peerId): + self.peerId = peerId + } + self.mode = mode - let currentTransactions = starsContext.currentState?.transactions ?? [] + let currentTransactions = self.starsContext?.currentState?.transactions ?? [] let initialTransactions: [StarsContext.State.Transaction] - switch subject { + switch mode { case .all: initialTransactions = currentTransactions case .incoming: @@ -464,41 +542,43 @@ private final class StarsTransactionsContextImpl { self._state = StarsTransactionsContext.State(transactions: initialTransactions, canLoadMore: true, isLoading: false) self._statePromise.set(.single(self._state)) - self.stateDisposable = (starsContext.state - |> deliverOnMainQueue).start(next: { [weak self] state in - guard let self, let state else { - return - } - - let currentTransactions = state.transactions - let filteredTransactions: [StarsContext.State.Transaction] - switch subject { - case .all: - filteredTransactions = currentTransactions - case .incoming: - filteredTransactions = currentTransactions.filter { $0.count > 0 } - case .outgoing: - filteredTransactions = currentTransactions.filter { $0.count < 0 } - } - - if filteredTransactions != initialTransactions { - var existingIds = Set() - for transaction in self._state.transactions { - if !transaction.flags.contains(.isLocal) { - existingIds.insert(transaction.id) - } + if let starsContext = self.starsContext { + self.stateDisposable = (starsContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self, let state else { + return } - - var updatedState = self._state - updatedState.transactions.removeAll(where: { $0.flags.contains(.isLocal) }) - for transaction in filteredTransactions.reversed() { - if !existingIds.contains(transaction.id) { - updatedState.transactions.insert(transaction, at: 0) + + let currentTransactions = state.transactions + let filteredTransactions: [StarsContext.State.Transaction] + switch mode { + case .all: + filteredTransactions = currentTransactions + case .incoming: + filteredTransactions = currentTransactions.filter { $0.count > 0 } + case .outgoing: + filteredTransactions = currentTransactions.filter { $0.count < 0 } + } + + if filteredTransactions != initialTransactions { + var existingIds = Set() + for transaction in self._state.transactions { + if !transaction.flags.contains(.isLocal) { + existingIds.insert(transaction.id) + } } + + var updatedState = self._state + updatedState.transactions.removeAll(where: { $0.flags.contains(.isLocal) }) + for transaction in filteredTransactions.reversed() { + if !existingIds.contains(transaction.id) { + updatedState.transactions.insert(transaction, at: 0) + } + } + self.updateState(updatedState) } - self.updateState(updatedState) - } - }) + }) + } } deinit { @@ -521,8 +601,8 @@ private final class StarsTransactionsContextImpl { var updatedState = self._state updatedState.isLoading = true self.updateState(updatedState) - - self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, subject: self.subject, offset: nextOffset) + + self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, mode: self.mode, offset: nextOffset, limit: self.nextOffset == "" ? 25 : 50) |> deliverOnMainQueue).start(next: { [weak self] status in guard let self else { return @@ -535,7 +615,7 @@ private final class StarsTransactionsContextImpl { updatedState.canLoadMore = self.nextOffset != nil self.updateState(updatedState) - if case .all = self.subject, nextOffset.isEmpty { + if case .all = self.mode, nextOffset.isEmpty { self.starsContext?.updateBalance(status.balance, transactions: status.transactions) } else { self.starsContext?.updateBalance(status.balance, transactions: nil) @@ -565,6 +645,11 @@ public final class StarsTransactionsContext { fileprivate let impl: QueueLocalObject public enum Subject { + case starsContext(StarsContext) + case peer(EnginePeer.Id) + } + + public enum Mode { case all case incoming case outgoing @@ -594,9 +679,9 @@ public final class StarsTransactionsContext { } } - init(account: Account, starsContext: StarsContext, subject: Subject) { + init(account: Account, subject: Subject, mode: Mode) { self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { - return StarsTransactionsContextImpl(account: account, starsContext: starsContext, subject: subject) + return StarsTransactionsContextImpl(account: account, subject: subject, mode: mode) }) } } @@ -671,6 +756,8 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot return .fail(.paymentFailed) } else if error.errorDescription == "INVOICE_ALREADY_PAID" { return .fail(.alreadyPaid) + } else if error.errorDescription == "MEDIA_ALREADY_PAID" { + return .fail(.alreadyPaid) } return .fail(.generic) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index fa68190c7c4..1640d568ca8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -70,13 +70,16 @@ public extension TelegramEngine { return _internal_starsTopUpOptions(account: self.account) } - public func peerStarsContext(peerId: EnginePeer.Id) -> StarsContext { - return StarsContext(account: self.account, peerId: peerId) + public func peerStarsContext() -> StarsContext { + return StarsContext(account: self.account) + } + + public func peerStarsRevenueContext(peerId: EnginePeer.Id) -> StarsRevenueStatsContext { + return StarsRevenueStatsContext(account: self.account, peerId: peerId) } - - public func peerStarsTransactionsContext(starsContext: StarsContext, subject: StarsTransactionsContext.Subject) -> StarsTransactionsContext { - return StarsTransactionsContext(account: self.account, starsContext: starsContext, subject: subject) + public func peerStarsTransactionsContext(subject: StarsTransactionsContext.Subject, mode: StarsTransactionsContext.Mode) -> StarsTransactionsContext { + return StarsTransactionsContext(account: self.account, subject: subject, mode: mode) } public func sendStarsPaymentForm(formId: Int64, source: BotPaymentInvoiceSource) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift index 9837949190f..3a8881c801c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift @@ -41,7 +41,7 @@ func _internal_inactiveChannelList(network: Network) -> Signal<[InactiveChannel] } var inactive: [InactiveChannel] = [] for (i, channel) in channels.enumerated() { - inactive.append(InactiveChannel(peer: channel, lastActivityDate: dates[i], participantsCount: participantsCounts[channel.id])) + inactive.append(InactiveChannel(peer: channel, lastActivityDate: i < dates.count ? dates[i] : 0, participantsCount: participantsCounts[channel.id])) } return inactive } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 6f1e12f55ba..6e32017ae92 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -561,10 +561,10 @@ public extension TelegramEngine { return _internal_removePeerChat(account: self.account, peerId: peerId, reportChatSpam: reportChatSpam, deleteGloballyIfPossible: deleteGloballyIfPossible) } - public func removePeerChats(peerIds: [PeerId]) -> Signal { + public func removePeerChats(peerIds: [PeerId], deleteGloballyIfPossible: Bool = false) -> Signal { return self.account.postbox.transaction { transaction -> Void in for peerId in peerIds { - _internal_removePeerChat(account: self.account, transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: peerId, reportChatSpam: false, deleteGloballyIfPossible: peerId.namespace == Namespaces.Peer.SecretChat) + _internal_removePeerChat(account: self.account, transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: peerId, reportChatSpam: false, deleteGloballyIfPossible: peerId.namespace == Namespaces.Peer.SecretChat || deleteGloballyIfPossible) } } |> ignoreValues @@ -823,6 +823,18 @@ public extension TelegramEngine { return _internal_requestChannelRevenueWithdrawalUrl(account: self.account, peerId: peerId, password: password) } + public func checkStarsRevenueWithdrawalAvailability() -> Signal { + return _internal_checkStarsRevenueWithdrawalAvailability(account: self.account) + } + + public func requestStarsRevenueWithdrawalUrl(peerId: EnginePeer.Id, amount: Int64, password: String) -> Signal { + return _internal_requestStarsRevenueWithdrawalUrl(account: self.account, peerId: peerId, amount: amount, password: password) + } + + public func requestStarsRevenueAdsAccountlUrl(peerId: EnginePeer.Id) -> Signal { + return _internal_requestStarsRevenueAdsAccountlUrl(account: self.account, peerId: peerId) + } + public func getChatListPeers(filterPredicate: ChatListFilterPredicate) -> Signal<[EnginePeer], NoError> { return self.account.postbox.transaction { transaction -> [EnginePeer] in return transaction.getChatListPeers(groupId: .root, filterPredicate: filterPredicate, additionalFilter: nil).map(EnginePeer.init) @@ -1475,7 +1487,7 @@ public extension TelegramEngine { return .single(false) } - return self.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: id, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 44, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: []) + return self.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: id, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: 44, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: []) |> map { view -> Bool in for entry in view.0.entries { if entry.message.flags.contains(.Incoming) { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 875d3ba68af..333ae2772aa 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -589,6 +589,9 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee if (flags2 & Int32(1 << 12)) != 0 { channelFlags.insert(.canViewRevenue) } + if (flags2 & Int32(1 << 14)) != 0 { + channelFlags.insert(.paidMediaAllowed) + } let sendAsPeerId = defaultSendAs?.peerId diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index 741799e6c31..e266dc6d115 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -526,6 +526,10 @@ public extension Message { } return nil } + + var paidContent: TelegramMediaPaidContent? { + return self.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent + } } public extension Message { diff --git a/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift b/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift index f47d7ea5fc5..3b76534e4e5 100644 --- a/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift +++ b/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift @@ -48,7 +48,7 @@ public extension ToolbarTheme { public extension NavigationBarTheme { convenience init(rootControllerTheme: PresentationTheme, enableBackgroundBlur: Bool = true, hideBackground: Bool = false, hideBadge: Bool = false, hideSeparator: Bool = false) { let theme = rootControllerTheme.rootController.navigationBar - self.init(buttonColor: theme.buttonColor, disabledButtonColor: theme.disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: hideBackground ? .clear : theme.blurredBackgroundColor, enableBackgroundBlur: enableBackgroundBlur, separatorColor: hideBackground || hideSeparator ? .clear : theme.separatorColor, badgeBackgroundColor: hideBadge ? .clear : theme.badgeBackgroundColor, badgeStrokeColor: hideBadge ? .clear : theme.badgeStrokeColor, badgeTextColor: hideBadge ? .clear : theme.badgeTextColor) + self.init(buttonColor: theme.buttonColor, disabledButtonColor: theme.disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: hideBackground ? .clear : theme.blurredBackgroundColor, opaqueBackgroundColor: hideBackground ? .clear : theme.opaqueBackgroundColor, enableBackgroundBlur: enableBackgroundBlur, separatorColor: hideBackground || hideSeparator ? .clear : theme.separatorColor, badgeBackgroundColor: hideBadge ? .clear : theme.badgeBackgroundColor, badgeStrokeColor: hideBadge ? .clear : theme.badgeStrokeColor, badgeTextColor: hideBadge ? .clear : theme.badgeTextColor) } } diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index df5ba1f61b0..4749936845e 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -531,6 +531,10 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionInactiveForeground: UIColor(rgb: 0xffffff), reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ), @@ -543,6 +547,10 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionInactiveForeground: UIColor(rgb: 0xffffff), reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ) @@ -561,6 +569,10 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionInactiveForeground: UIColor(rgb: 0xffffff), reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ), @@ -573,6 +585,10 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionInactiveForeground: UIColor(rgb: 0xffffff), reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ) @@ -588,6 +604,10 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionInactiveForeground: UIColor(rgb: 0xffffff), reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ), @@ -600,6 +620,10 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionInactiveForeground: UIColor(rgb: 0xffffff), reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ) diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift index 337c7188aa3..24cd2164c4e 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift @@ -744,6 +744,10 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres reactionInactiveForeground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveBackground: accentColor, reactionActiveForeground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ), @@ -756,6 +760,10 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres reactionInactiveForeground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveBackground: accentColor, reactionActiveForeground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ) @@ -772,6 +780,10 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres reactionInactiveForeground: UIColor(rgb: 0xffffff), reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ), @@ -784,6 +796,10 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres reactionInactiveForeground: UIColor(rgb: 0xffffff), reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ) @@ -799,6 +815,10 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres reactionInactiveForeground: UIColor(rgb: 0xffffff), reactionActiveBackground: accentColor, reactionActiveForeground: UIColor(rgb: 0xffffff), + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ), @@ -811,6 +831,10 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres reactionInactiveForeground: UIColor(rgb: 0xffffff), reactionActiveBackground: accentColor, reactionActiveForeground: UIColor(rgb: 0xffffff), + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ) diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index f13d955c4be..6e8c85faa96 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -16,7 +16,11 @@ public func selectDateFillStaticColor(theme: PresentationTheme, wallpaper: Teleg } } -public func selectReactionFillStaticColor(theme: PresentationTheme, wallpaper: TelegramWallpaper) -> UIColor { +public func selectReactionFillStaticColor(theme: PresentationTheme, wallpaper: TelegramWallpaper, isStars: Bool = false) -> UIColor { + if isStars { + return theme.chat.message.freeform.withoutWallpaper.reactionStarsInactiveBackground + } + if case .color = wallpaper { return theme.chat.message.freeform.withoutWallpaper.reactionInactiveBackground } else if theme.overallDarkAppearance { @@ -594,6 +598,10 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionInactiveForeground: defaultDayAccentColor, reactionActiveBackground: defaultDayAccentColor, reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ), @@ -606,6 +614,10 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionInactiveForeground: defaultDayAccentColor, reactionActiveBackground: defaultDayAccentColor, reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ) @@ -641,6 +653,10 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionInactiveForeground: UIColor(rgb: 0x3fc33b), reactionActiveBackground: UIColor(rgb: 0x3fc33b), reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ), @@ -653,6 +669,10 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionInactiveForeground: UIColor(rgb: 0x3fc33b), reactionActiveBackground: UIColor(rgb: 0x3fc33b), reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ) @@ -690,6 +710,10 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionInactiveForeground: UIColor(rgb: 0xffffff), reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 0.8), reactionActiveForeground: UIColor(white: 0.0, alpha: 0.1), + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ), @@ -702,6 +726,10 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionInactiveForeground: UIColor(rgb: 0xffffff), reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 0.8), reactionActiveForeground: UIColor(white: 0.0, alpha: 0.1), + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ) @@ -734,6 +762,10 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionInactiveForeground: defaultDayAccentColor, reactionActiveBackground: defaultDayAccentColor, reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ), @@ -746,6 +778,10 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionInactiveForeground: defaultDayAccentColor, reactionActiveBackground: defaultDayAccentColor, reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ) @@ -784,6 +820,10 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionInactiveForeground: UIColor(rgb: 0xffffff), reactionActiveBackground: UIColor(rgb: 0xffffff), reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ), @@ -796,6 +836,10 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionInactiveForeground: UIColor(rgb: 0xffffff), reactionActiveBackground: UIColor(rgb: 0xffffff), reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ) @@ -833,6 +877,10 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionInactiveForeground: defaultDayAccentColor, reactionActiveBackground: defaultDayAccentColor, reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ), @@ -845,6 +893,10 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionInactiveForeground: defaultDayAccentColor, reactionActiveBackground: defaultDayAccentColor, reactionActiveForeground: .clear, + reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), + reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), + reactionStarsActiveForeground: .clear, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ) diff --git a/submodules/TelegramPresentationData/Sources/PresentationData.swift b/submodules/TelegramPresentationData/Sources/PresentationData.swift index 11f2f15c3b4..8ad523ddd10 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationData.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationData.swift @@ -69,11 +69,13 @@ public struct PresentationChatBubbleCorners: Equatable, Hashable { public var mainRadius: CGFloat public var auxiliaryRadius: CGFloat public var mergeBubbleCorners: Bool + public var hasTails: Bool - public init(mainRadius: CGFloat, auxiliaryRadius: CGFloat, mergeBubbleCorners: Bool) { + public init(mainRadius: CGFloat, auxiliaryRadius: CGFloat, mergeBubbleCorners: Bool, hasTails: Bool = true) { self.mainRadius = mainRadius self.auxiliaryRadius = auxiliaryRadius self.mergeBubbleCorners = mergeBubbleCorners + self.hasTails = hasTails } } diff --git a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift index fb7f9057722..2f71522c772 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift @@ -783,6 +783,10 @@ public final class PresentationThemeBubbleColorComponents { public let reactionInactiveForeground: UIColor public let reactionActiveBackground: UIColor public let reactionActiveForeground: UIColor + public let reactionStarsInactiveBackground: UIColor + public let reactionStarsInactiveForeground: UIColor + public let reactionStarsActiveBackground: UIColor + public let reactionStarsActiveForeground: UIColor public let reactionInactiveMediaPlaceholder: UIColor public let reactionActiveMediaPlaceholder: UIColor @@ -795,6 +799,10 @@ public final class PresentationThemeBubbleColorComponents { reactionInactiveForeground: UIColor, reactionActiveBackground: UIColor, reactionActiveForeground: UIColor, + reactionStarsInactiveBackground: UIColor, + reactionStarsInactiveForeground: UIColor, + reactionStarsActiveBackground: UIColor, + reactionStarsActiveForeground: UIColor, reactionInactiveMediaPlaceholder: UIColor, reactionActiveMediaPlaceholder: UIColor ) { @@ -806,6 +814,10 @@ public final class PresentationThemeBubbleColorComponents { self.reactionInactiveForeground = reactionInactiveForeground self.reactionActiveBackground = reactionActiveBackground self.reactionActiveForeground = reactionActiveForeground + self.reactionStarsInactiveBackground = reactionStarsInactiveBackground + self.reactionStarsInactiveForeground = reactionStarsInactiveForeground + self.reactionStarsActiveBackground = reactionStarsActiveBackground + self.reactionStarsActiveForeground = reactionStarsActiveForeground self.reactionInactiveMediaPlaceholder = reactionInactiveMediaPlaceholder self.reactionActiveMediaPlaceholder = reactionActiveMediaPlaceholder } @@ -818,6 +830,10 @@ public final class PresentationThemeBubbleColorComponents { reactionInactiveForeground: UIColor? = nil, reactionActiveBackground: UIColor? = nil, reactionActiveForeground: UIColor? = nil, + reactionStarsInactiveBackground: UIColor? = nil, + reactionStarsInactiveForeground: UIColor? = nil, + reactionStarsActiveBackground: UIColor? = nil, + reactionStarsActiveForeground: UIColor? = nil, reactionInactiveMediaPlaceholder: UIColor? = nil, reactionActiveMediaPlaceholder: UIColor? = nil ) -> PresentationThemeBubbleColorComponents { @@ -830,6 +846,10 @@ public final class PresentationThemeBubbleColorComponents { reactionInactiveForeground: reactionInactiveForeground ?? self.reactionInactiveForeground, reactionActiveBackground: reactionActiveBackground ?? self.reactionActiveBackground, reactionActiveForeground: reactionActiveForeground ?? self.reactionActiveForeground, + reactionStarsInactiveBackground: reactionStarsInactiveBackground ?? self.reactionStarsInactiveBackground, + reactionStarsInactiveForeground: reactionStarsInactiveForeground ?? self.reactionStarsInactiveForeground, + reactionStarsActiveBackground: reactionStarsActiveBackground ?? self.reactionStarsActiveBackground, + reactionStarsActiveForeground: reactionStarsActiveForeground ?? self.reactionStarsActiveForeground, reactionInactiveMediaPlaceholder: reactionInactiveMediaPlaceholder ?? self.reactionInactiveMediaPlaceholder, reactionActiveMediaPlaceholder: reactionActiveMediaPlaceholder ?? self.reactionActiveMediaPlaceholder ) diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift index 591e5e131b0..a5e38bb28f4 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift @@ -1206,6 +1206,10 @@ extension PresentationThemeBubbleColorComponents: Codable { reactionInactiveForeground: reactionInactiveForeground, reactionActiveBackground: reactionActiveBackground, reactionActiveForeground: reactionActiveForeground, + reactionStarsInactiveBackground: reactionInactiveBackground, + reactionStarsInactiveForeground: reactionInactiveForeground, + reactionStarsActiveBackground: reactionActiveBackground, + reactionStarsActiveForeground: reactionActiveForeground, reactionInactiveMediaPlaceholder: reactionInactiveMediaPlaceholder, reactionActiveMediaPlaceholder: reactionActiveMediaPlaceholder ) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index 4fffe198997..79432e2c4ca 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -146,6 +146,24 @@ public struct PresentationResourcesSettings { drawBorder(context: context, rect: bounds) }) + + public static let bot = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let path = UIBezierPath(roundedRect: bounds, cornerRadius: 7.0) + context.addPath(path.cgPath) + context.clip() + + context.setFillColor(UIColor(rgb: 0x007aff).cgColor) + context.fill(bounds) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Bot"), color: UIColor(rgb: 0xffffff)), let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - image.size.width) / 2.0), y: floorToScreenPixels((bounds.height - image.size.height) / 2.0)), size: image.size)) + } + + drawBorder(context: context, rect: bounds) + }) public static let passport = renderIcon(name: "Settings/Menu/Passport") public static let watch = renderIcon(name: "Settings/Menu/Watch") diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index 00668606e84..9cba0d586f7 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -29,6 +29,7 @@ public enum MessageContentKindKey { case invoice case story case giveaway + case paidContent } public enum MessageContentKind: Equatable { @@ -386,6 +387,25 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil } else { return nil } + case let .paidContent(paidContent): + switch paidContent.extendedMedia.first { + case let .preview(_, _, videoDuration): + if let _ = videoDuration { + return .video + } else { + return .image + } + case let .full(media): + if media is TelegramMediaImage { + return .image + } else if media is TelegramMediaFile { + return .video + } else { + return nil + } + default: + return nil + } default: return nil } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index b343de0591f..cf7fd661960 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -550,7 +550,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, if currency == "XTR" { let amountAttributedString = NSMutableAttributedString(string: "#\(totalAmount)", font: titleBoldFont, textColor: primaryTextColor) if let range = amountAttributedString.string.range(of: "#") { - amountAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars), range: NSRange(range, in: amountAttributedString.string)) + amountAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: amountAttributedString.string)) amountAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: amountAttributedString.string)) } mutableString.replaceCharacters(in: range, with: amountAttributedString) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index d231828b10a..636f4ea0b78 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -482,7 +482,12 @@ swift_library( "//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/PeerManagement/OwnershipTransferController", + "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", + "//submodules/TelegramUI/Components/Chat/ChatSendStarsScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/ActionPanelComponent/Sources/ActionPanelComponent.swift b/submodules/TelegramUI/Components/ActionPanelComponent/Sources/ActionPanelComponent.swift index 2e946f37dbe..988c96b31f0 100644 --- a/submodules/TelegramUI/Components/ActionPanelComponent/Sources/ActionPanelComponent.swift +++ b/submodules/TelegramUI/Components/ActionPanelComponent/Sources/ActionPanelComponent.swift @@ -126,7 +126,7 @@ public final class ActionPanelComponent: Component { return super.hitTest(point, with: event) } - func update(component: ActionPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ActionPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme self.component = component @@ -185,7 +185,7 @@ public final class ActionPanelComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift index 47fa71b51f1..c6759c8c062 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift @@ -135,7 +135,7 @@ final class AdminUserActionsPeerComponent: Component { component.action(peer) } - func update(component: AdminUserActionsPeerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: AdminUserActionsPeerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme var hasSelectionUpdated = false @@ -270,7 +270,7 @@ final class AdminUserActionsPeerComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift index 150c2077763..b85e764df7e 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift @@ -435,7 +435,7 @@ private final class AdminUserActionsSheetComponent: Component { ) } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { return } @@ -497,7 +497,7 @@ private final class AdminUserActionsSheetComponent: Component { } } - func update(component: AdminUserActionsSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: AdminUserActionsSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -859,7 +859,7 @@ private final class AdminUserActionsSheetComponent: Component { self.optionBanSelectedPeers = selectedPeers } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) } )))) } @@ -1400,7 +1400,7 @@ private final class AdminUserActionsSheetComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -1524,7 +1524,7 @@ private final class OptionSectionExpandIndicatorComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: OptionSectionExpandIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: OptionSectionExpandIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let countArrowSpacing: CGFloat = 1.0 let iconCountSpacing: CGFloat = 1.0 @@ -1578,7 +1578,7 @@ private final class OptionSectionExpandIndicatorComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -1627,7 +1627,7 @@ private final class MediaSectionExpandIndicatorComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: MediaSectionExpandIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: MediaSectionExpandIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let titleArrowSpacing: CGFloat = 1.0 if self.arrowView.image == nil { @@ -1669,7 +1669,7 @@ private final class MediaSectionExpandIndicatorComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -1728,7 +1728,7 @@ private final class OptionsSectionFooterComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: OptionsSectionFooterComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: OptionsSectionFooterComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.arrowView.image == nil { self.arrowView.image = PresentationResourcesItemList.expandSmallDownArrowImage(component.theme) } @@ -1761,7 +1761,7 @@ private final class OptionsSectionFooterComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift index 2a3c17d32a3..b6a138bce92 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift @@ -369,7 +369,7 @@ private final class RecentActionsSettingsSheetComponent: Component { ) } - private func updateScrolling(isFirstTime: Bool = false, transition: Transition) { + private func updateScrolling(isFirstTime: Bool = false, transition: ComponentTransition) { guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { return } @@ -438,7 +438,7 @@ private final class RecentActionsSettingsSheetComponent: Component { } } - func update(component: RecentActionsSettingsSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: RecentActionsSettingsSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -775,7 +775,7 @@ private final class RecentActionsSettingsSheetComponent: Component { self.selectedAdmins.insert(peer.id) } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.35, curve: .easeInOut))) } )))) } @@ -792,7 +792,7 @@ private final class RecentActionsSettingsSheetComponent: Component { self.selectedAdmins.removeAll() } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.35, curve: .easeInOut))) } adminsSectionItems.append(AnyComponentWithIdentity(id: adminsSectionItems.count, component: AnyComponent(ListActionItemComponent( theme: environment.theme, @@ -944,7 +944,7 @@ private final class RecentActionsSettingsSheetComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -1051,7 +1051,7 @@ private final class MediaSectionExpandIndicatorComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: MediaSectionExpandIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: MediaSectionExpandIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let titleArrowSpacing: CGFloat = 1.0 if self.arrowView.image == nil { @@ -1093,7 +1093,7 @@ private final class MediaSectionExpandIndicatorComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift index cb1bcc02fba..17359fffd5e 100644 --- a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift +++ b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift @@ -256,7 +256,7 @@ private final class SheetContent: CombinedComponent { let openMore: () -> Void let complete: (ReportResult) -> Void let dismiss: () -> Void - let update: (Transition) -> Void + let update: (ComponentTransition) -> Void init( context: AccountContext, @@ -268,7 +268,7 @@ private final class SheetContent: CombinedComponent { openMore: @escaping () -> Void, complete: @escaping (ReportResult) -> Void, dismiss: @escaping () -> Void, - update: @escaping (Transition) -> Void + update: @escaping (ComponentTransition) -> Void ) { self.context = context self.peerId = peerId @@ -659,7 +659,7 @@ public final class AdsReportScreen: ViewControllerComponentContainer { private final class NavigationContainer: UIView, UIGestureRecognizerDelegate { - var requestUpdate: ((Transition) -> Void)? + var requestUpdate: ((ComponentTransition) -> Void)? var requestPop: (() -> Void)? var transitionFraction: CGFloat = 0.0 @@ -772,10 +772,10 @@ final class NavigationStackComponent: Component { var index: Int var itemId: AnyHashable var itemView: ItemView - var itemTransition: Transition + var itemTransition: ComponentTransition var itemSize: CGSize - init(index: Int, itemId: AnyHashable, itemView: ItemView, itemTransition: Transition, itemSize: CGSize) { + init(index: Int, itemId: AnyHashable, itemView: ItemView, itemTransition: ComponentTransition, itemSize: CGSize) { self.index = index self.itemId = itemId self.itemView = itemView @@ -815,7 +815,7 @@ final class NavigationStackComponent: Component { preconditionFailure() } - func update(component: NavigationStackComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: NavigationStackComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -945,7 +945,7 @@ final class NavigationStackComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/AnimatedCounterComponent/Sources/AnimatedCounterComponent.swift b/submodules/TelegramUI/Components/AnimatedCounterComponent/Sources/AnimatedCounterComponent.swift index 8f77f6976a5..2a8375e4430 100644 --- a/submodules/TelegramUI/Components/AnimatedCounterComponent/Sources/AnimatedCounterComponent.swift +++ b/submodules/TelegramUI/Components/AnimatedCounterComponent/Sources/AnimatedCounterComponent.swift @@ -61,7 +61,7 @@ final class AnimatedCounterItemComponent: Component { preconditionFailure() } - func update(component: AnimatedCounterItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: AnimatedCounterItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousNumericValue = self.component?.numericValue self.component = component @@ -92,7 +92,7 @@ final class AnimatedCounterItemComponent: Component { let offsetY: CGFloat = size.height * 0.6 * (previousNumericValue < component.numericValue ? -1.0 : 1.0) - let subTransition = Transition(animation: .curve(duration: 0.16, curve: .easeInOut)) + 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) @@ -111,7 +111,7 @@ final class AnimatedCounterItemComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -188,7 +188,7 @@ public final class AnimatedCounterComponent: Component { preconditionFailure() } - func update(component: AnimatedCounterComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: AnimatedCounterComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let spaceWidth: CGFloat if let measuredSpaceWidth = self.measuredSpaceWidth, let previousComponent = self.component, previousComponent.font.pointSize == component.font.pointSize { spaceWidth = measuredSpaceWidth @@ -273,7 +273,7 @@ public final class AnimatedCounterComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift index d30ca5dcb41..5c3a6f05467 100644 --- a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift +++ b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift @@ -68,7 +68,7 @@ public final class AnimatedTextComponent: Component { preconditionFailure() } - func update(component: AnimatedTextComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: AnimatedTextComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -190,7 +190,7 @@ public final class AnimatedTextComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift b/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift index 3696b9ffeaa..0433cbae598 100644 --- a/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift +++ b/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift @@ -99,7 +99,7 @@ public final class AudioTranscriptionButtonComponent: Component { self.component?.pressed() } - func update(component: AudioTranscriptionButtonComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: AudioTranscriptionButtonComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let size = CGSize(width: 30.0, height: 30.0) let foregroundColor: UIColor @@ -273,7 +273,7 @@ public final class AudioTranscriptionButtonComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/TelegramUI/Components/AudioTranscriptionPendingIndicatorComponent/Sources/AudioTranscriptionPendingIndicatorComponent.swift b/submodules/TelegramUI/Components/AudioTranscriptionPendingIndicatorComponent/Sources/AudioTranscriptionPendingIndicatorComponent.swift index e45f38fbbb8..a4427eef43d 100644 --- a/submodules/TelegramUI/Components/AudioTranscriptionPendingIndicatorComponent/Sources/AudioTranscriptionPendingIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/AudioTranscriptionPendingIndicatorComponent/Sources/AudioTranscriptionPendingIndicatorComponent.swift @@ -63,7 +63,7 @@ public final class AudioTranscriptionPendingIndicatorComponent: Component { } } - func update(component: AudioTranscriptionPendingIndicatorComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: AudioTranscriptionPendingIndicatorComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let dotSize: CGFloat = 2.0 let spacing: CGFloat = 3.0 @@ -95,7 +95,7 @@ public final class AudioTranscriptionPendingIndicatorComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -134,7 +134,7 @@ public final class AudioTranscriptionPendingLottieIndicatorComponent: Component fatalError("init(coder:) has not been implemented") } - func update(component: AudioTranscriptionPendingLottieIndicatorComponent, availableSize: CGSize, transition: Transition) -> CGSize { + 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( @@ -171,7 +171,7 @@ public final class AudioTranscriptionPendingLottieIndicatorComponent: Component return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/TelegramUI/Components/AudioWaveformComponent/Sources/AudioWaveformComponent.swift b/submodules/TelegramUI/Components/AudioWaveformComponent/Sources/AudioWaveformComponent.swift index 2eb94f5e3f2..d1f84dd356d 100644 --- a/submodules/TelegramUI/Components/AudioWaveformComponent/Sources/AudioWaveformComponent.swift +++ b/submodules/TelegramUI/Components/AudioWaveformComponent/Sources/AudioWaveformComponent.swift @@ -364,7 +364,7 @@ public final class AudioWaveformComponent: Component { } } - func update(component: AudioWaveformComponent, availableSize: CGSize, transition: Transition) -> CGSize { + 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 { @@ -650,7 +650,7 @@ public final class AudioWaveformComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index 563cd08d21c..9e19fb46b41 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -686,7 +686,7 @@ final class AvatarEditorScreenComponent: Component { self.endEditing(true) if let state = self.state, state.expanded { state.expanded = false - state.updated(transition: Transition(animation: .curve(duration: 0.45, curve: .spring))) + state.updated(transition: ComponentTransition(animation: .curve(duration: 0.45, curve: .spring))) } } }, @@ -817,7 +817,7 @@ final class AvatarEditorScreenComponent: Component { self.endEditing(true) if let state = self.state, state.expanded { state.expanded = false - state.updated(transition: Transition(animation: .curve(duration: 0.45, curve: .spring))) + state.updated(transition: ComponentTransition(animation: .curve(duration: 0.45, curve: .spring))) } } }, @@ -839,7 +839,7 @@ final class AvatarEditorScreenComponent: Component { private var isExpanded = false - func update(component: AvatarEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + func update(component: AvatarEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -983,7 +983,7 @@ final class AvatarEditorScreenComponent: Component { emojiView.ensureSearchUnfocused() } state.expanded = !state.expanded - state.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + state.updated(transition: ComponentTransition(animation: .curve(duration: 0.35, curve: .spring))) } } ) @@ -1491,7 +1491,7 @@ final class AvatarEditorScreenComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarPreviewComponent.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarPreviewComponent.swift index 3fe58e72e37..cc76d661fb4 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarPreviewComponent.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarPreviewComponent.swift @@ -93,7 +93,7 @@ final class AvatarPreviewComponent: Component { self.component?.tapped() } - func update(component: AvatarPreviewComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: AvatarPreviewComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousBackground = self.component?.background let hadFile = self.component?.file != nil @@ -220,7 +220,7 @@ final class AvatarPreviewComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/BackgroundColorComponent.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/BackgroundColorComponent.swift index d56e398b9e4..169030cb089 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/BackgroundColorComponent.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/BackgroundColorComponent.swift @@ -63,7 +63,7 @@ final class BackgroundColorComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: BackgroundColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BackgroundColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -125,7 +125,7 @@ final class BackgroundColorComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -242,7 +242,7 @@ final class BackgroundSwatchComponent: Component { super.cancelTracking(with: event) } - func update(component: BackgroundSwatchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BackgroundSwatchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousBackground = self.component?.background self.component = component @@ -313,7 +313,7 @@ final class BackgroundSwatchComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/ColorPickerComponent.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/ColorPickerComponent.swift index 73de5e88197..100c74d727f 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/ColorPickerComponent.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/ColorPickerComponent.swift @@ -395,7 +395,7 @@ final class ColorPickerComponent: Component { } private var component: ColorPickerComponent? - func update(component: ColorPickerComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: ColorPickerComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let themeChanged = self.component?.theme !== component.theme let previousIsVisible = self.component?.isVisible ?? false self.component = component @@ -448,7 +448,7 @@ final class ColorPickerComponent: Component { return View(theme: self.theme, strings: self.strings) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/TelegramUI/Components/BackButtonComponent/Sources/BackButtonComponent.swift b/submodules/TelegramUI/Components/BackButtonComponent/Sources/BackButtonComponent.swift index 88379e41999..2838a44c68b 100644 --- a/submodules/TelegramUI/Components/BackButtonComponent/Sources/BackButtonComponent.swift +++ b/submodules/TelegramUI/Components/BackButtonComponent/Sources/BackButtonComponent.swift @@ -67,7 +67,7 @@ public final class BackButtonComponent: Component { return super.hitTest(point, with: event) } - func update(component: BackButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BackButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let sideInset: CGFloat = 4.0 let titleSize = self.title.update( @@ -97,7 +97,7 @@ public final class BackButtonComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/BottomButtonPanelComponent/Sources/BottomButtonPanelComponent.swift b/submodules/TelegramUI/Components/BottomButtonPanelComponent/Sources/BottomButtonPanelComponent.swift index 004611ea4d7..4e56d8d03f1 100644 --- a/submodules/TelegramUI/Components/BottomButtonPanelComponent/Sources/BottomButtonPanelComponent.swift +++ b/submodules/TelegramUI/Components/BottomButtonPanelComponent/Sources/BottomButtonPanelComponent.swift @@ -71,7 +71,7 @@ public final class BottomButtonPanelComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: BottomButtonPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BottomButtonPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme self.component = component @@ -142,7 +142,7 @@ public final class BottomButtonPanelComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift index f23ba40ed20..f3c401fcbb4 100644 --- a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift +++ b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift @@ -52,7 +52,7 @@ public final class ButtonBadgeComponent: Component { fatalError("init(coder:) has not been implemented") } - public func update(component: ButtonBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(component: ButtonBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let height: CGFloat switch component.style { case .round: @@ -100,7 +100,7 @@ public final class ButtonBadgeComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -187,7 +187,7 @@ public final class ButtonTextContentComponent: Component { return super.hitTest(point, with: event) } - func update(component: ButtonTextContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ButtonTextContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousBadge = self.component?.badge self.component = component @@ -324,7 +324,7 @@ public final class ButtonTextContentComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -447,7 +447,7 @@ public final class ButtonComponent: Component { return super.hitTest(point, with: event) } - func update(component: ButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.componentState = state @@ -546,7 +546,7 @@ public final class ButtonComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Metal/CallScreenShaders.metal b/submodules/TelegramUI/Components/Calls/CallScreen/Metal/CallScreenShaders.metal index 1402203f290..c850eeada51 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Metal/CallScreenShaders.metal +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Metal/CallScreenShaders.metal @@ -230,10 +230,10 @@ vertex BlobVertexOut callBlobVertex( } fragment half4 callBlobFragment( - BlobVertexOut in [[stage_in]] + BlobVertexOut in [[stage_in]], + const device float4 &color [[ buffer(0) ]] ) { - half alpha = 0.35; - return half4(1.0 * alpha, 1.0 * alpha, 1.0 * alpha, alpha); + return half4(color.r * color.a, color.g * color.a, color.b * color.a, color.a); } kernel void videoBiPlanarToRGBA( diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/AvatarLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/AvatarLayer.swift index d08c25a5fc7..0164a3f8003 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/AvatarLayer.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/AvatarLayer.swift @@ -71,7 +71,7 @@ final class AvatarLayer: SimpleLayer { } } - func update(size: CGSize, isExpanded: Bool, cornerRadius: CGFloat, transition: Transition) { + func update(size: CGSize, isExpanded: Bool, cornerRadius: CGFloat, transition: ComponentTransition) { let params = Params(size: size, cornerRadius: cornerRadius, isExpanded: isExpanded) if self.params == params { return diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift index 73c36b48cec..4c731b68f83 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift @@ -86,7 +86,7 @@ final class ButtonGroupView: OverlayMaskContainerView { return result } - func update(size: CGSize, insets: UIEdgeInsets, minWidth: CGFloat, controlsHidden: Bool, displayClose: Bool, strings: PresentationStrings, buttons: [Button], notices: [Notice], transition: Transition) -> CGFloat { + func update(size: CGSize, insets: UIEdgeInsets, minWidth: CGFloat, controlsHidden: Bool, displayClose: Bool, strings: PresentationStrings, buttons: [Button], notices: [Notice], transition: ComponentTransition) -> CGFloat { self.buttons = buttons let buttonSize: CGFloat = 56.0 @@ -284,7 +284,7 @@ final class ButtonGroupView: OverlayMaskContainerView { button.action() } - Transition.immediate.setScale(view: buttonView, scale: 0.001) + ComponentTransition.immediate.setScale(view: buttonView, scale: 0.001) buttonView.alpha = 0.0 transition.setScale(view: buttonView, scale: 1.0) } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBackgroundLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBackgroundLayer.swift index 26ffbe48325..c1673251fd4 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBackgroundLayer.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBackgroundLayer.swift @@ -170,7 +170,7 @@ final class CallBackgroundLayer: MetalEngineSubjectLayer, MetalEngineSubject { fatalError("init(coder:) has not been implemented") } - func update(stateIndex: Int, isEnergySavingEnabled: Bool, transition: Transition) { + func update(stateIndex: Int, isEnergySavingEnabled: Bool, transition: ComponentTransition) { self.isEnergySavingEnabled = isEnergySavingEnabled if self.stateIndex != stateIndex { diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift index 4747301e7ac..1fcba0be180 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift @@ -3,14 +3,17 @@ import MetalKit import MetalEngine import Display -final class CallBlobsLayer: MetalEngineSubjectLayer, MetalEngineSubject { - var internalData: MetalEngineSubjectInternalData? +public final class CallBlobsLayer: MetalEngineSubjectLayer, MetalEngineSubject { + public var internalData: MetalEngineSubjectInternalData? - struct Blob { + private struct Blob { + var color: SIMD4 var points: [Float] var nextPoints: [Float] - init(count: Int) { + init(count: Int, color: SIMD4) { + self.color = color + self.points = (0 ..< count).map { _ in Float.random(in: 0.0 ... 1.0) } @@ -35,7 +38,7 @@ final class CallBlobsLayer: MetalEngineSubjectLayer, MetalEngineSubject { } } - final class RenderState: RenderToLayerState { + private final class RenderState: RenderToLayerState { let pipelineState: MTLRenderPipelineState required init?(device: MTLDevice) { @@ -71,7 +74,7 @@ final class CallBlobsLayer: MetalEngineSubjectLayer, MetalEngineSubject { private var displayLinkSubscription: SharedDisplayLinkDriver.Link? - override init() { + public init(colors: [UIColor] = [UIColor(white: 1.0, alpha: 0.35), UIColor(white: 1.0, alpha: 0.35)]) { super.init() self.didEnterHierarchy = { [weak self] in @@ -100,20 +103,26 @@ final class CallBlobsLayer: MetalEngineSubjectLayer, MetalEngineSubject { } self.isOpaque = false - self.blobs = (0 ..< 2).map { _ in - Blob(count: 8) + self.blobs = colors.reversed().map { color in + var r: CGFloat = 0.0 + var g: CGFloat = 0.0 + var b: CGFloat = 0.0 + var a: CGFloat = 0.0 + color.getRed(&r, green: &g, blue: &b, alpha: &a) + + return Blob(count: 8, color: SIMD4(Float(r), Float(g), Float(b), Float(a))) } } - override init(layer: Any) { + override public init(layer: Any) { super.init(layer: layer) } - required init?(coder: NSCoder) { + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func update(context: MetalEngineSubjectContext) { + public func update(context: MetalEngineSubjectContext) { if self.bounds.isEmpty { return } @@ -137,6 +146,9 @@ final class CallBlobsLayer: MetalEngineSubjectLayer, MetalEngineSubject { encoder.setVertexBytes(&points, length: MemoryLayout.size * points.count, index: 1) encoder.setVertexBytes(&count, length: MemoryLayout.size, index: 2) + var color = blobs[i].color + encoder.setFragmentBytes(&color, length: MemoryLayout.size * 4, index: 0) + encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3 * 8 * points.count) } }) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CloseButtonView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CloseButtonView.swift index 110814ead64..78212265b44 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CloseButtonView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CloseButtonView.swift @@ -88,13 +88,13 @@ final class CloseButtonView: HighlightTrackingButton, OverlayMaskContainerViewPr if highlighted { self.layer.removeAnimation(forKey: "sublayerTransform") - let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.15, curve: .easeInOut)) transition.setScale(layer: self.layer, scale: topScale) } else { let t = self.layer.presentation()?.transform ?? layer.transform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) - let transition = Transition(animation: .none) + let transition = ComponentTransition(animation: .none) transition.setScale(layer: self.layer, scale: 1.0) self.layer.animateScale(from: currentScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] completed in @@ -149,7 +149,7 @@ final class CloseButtonView: HighlightTrackingButton, OverlayMaskContainerViewPr }) } - func update(text: String, size: CGSize, transition: Transition) { + func update(text: String, size: CGSize, transition: ComponentTransition) { let params = Params(text: text, size: size) if self.params == params { return @@ -158,7 +158,7 @@ final class CloseButtonView: HighlightTrackingButton, OverlayMaskContainerViewPr self.update(params: params, transition: transition) } - private func update(params: Params, transition: Transition) { + private func update(params: Params, transition: ComponentTransition) { let fillFraction: CGFloat = CGFloat(self.fillTime / self.duration) let sideInset: CGFloat = 12.0 diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift index ac1ea32db8b..2a0752f4def 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift @@ -66,13 +66,13 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV if highlighted { self.layer.removeAnimation(forKey: "sublayerTransform") - let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.15, curve: .easeInOut)) transition.setScale(layer: self.layer, scale: topScale) } else { let t = self.layer.presentation()?.transform ?? layer.transform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) - let transition = Transition(animation: .none) + let transition = ComponentTransition(animation: .none) transition.setScale(layer: self.layer, scale: 1.0) self.layer.animateScale(from: currentScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] completed in @@ -95,7 +95,7 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV self.action?() } - func update(size: CGSize, image: UIImage?, isSelected: Bool, isDestructive: Bool, isEnabled: Bool, title: String, transition: Transition) { + func update(size: CGSize, image: UIImage?, isSelected: Bool, isDestructive: Bool, isEnabled: Bool, title: String, transition: ComponentTransition) { let contentParams = ContentParams(size: size, image: image, isSelected: isSelected, isDestructive: isDestructive, isEnabled: isEnabled) if self.contentParams != contentParams { self.contentParams = contentParams @@ -110,7 +110,7 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV self.textView.frame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) * 0.5), y: size.height + 4.0), size: textSize) } - private func updateContent(contentParams: ContentParams, transition: Transition) { + private func updateContent(contentParams: ContentParams, transition: ComponentTransition) { let image = generateImage(contentParams.size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiExpandedInfoView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiExpandedInfoView.swift index f2b0ffd7ec1..13b0d2346b6 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiExpandedInfoView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiExpandedInfoView.swift @@ -84,13 +84,13 @@ final class EmojiExpandedInfoView: OverlayMaskContainerView { if highlighted { self.actionButton.layer.removeAnimation(forKey: "sublayerTransform") - let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.15, curve: .easeInOut)) transition.setScale(layer: self.actionButton.layer, scale: topScale) } else { let t = self.actionButton.layer.presentation()?.transform ?? layer.transform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) - let transition = Transition(animation: .none) + let transition = ComponentTransition(animation: .none) transition.setScale(layer: self.actionButton.layer, scale: 1.0) self.actionButton.layer.animateScale(from: currentScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] completed in @@ -129,7 +129,7 @@ final class EmojiExpandedInfoView: OverlayMaskContainerView { return nil } - func update(width: CGFloat, transition: Transition) -> CGSize { + func update(width: CGFloat, transition: ComponentTransition) -> CGSize { let params = Params(width: width) if let currentLayout = self.currentLayout, currentLayout.params == params { return currentLayout.size @@ -139,7 +139,7 @@ final class EmojiExpandedInfoView: OverlayMaskContainerView { return size } - private func update(params: Params, transition: Transition) -> CGSize { + private func update(params: Params, transition: ComponentTransition) -> CGSize { let buttonHeight: CGFloat = 56.0 let titleSize = self.titleView.update(string: self.title, fontSize: 16.0, fontWeight: 0.3, alignment: .center, color: .white, constrainedWidth: params.width - 16.0 * 2.0, transition: transition) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/KeyEmojiView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/KeyEmojiView.swift index 2e50530470e..557c56a4d15 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/KeyEmojiView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/KeyEmojiView.swift @@ -55,13 +55,13 @@ final class KeyEmojiView: HighlightTrackingButton { if highlighted { self.layer.removeAnimation(forKey: "opacity") self.layer.removeAnimation(forKey: "transform") - let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.15, curve: .easeInOut)) transition.setScale(layer: self.layer, scale: topScale) } else { let t = self.layer.presentation()?.transform ?? layer.transform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) - let transition = Transition(animation: .none) + let transition = ComponentTransition(animation: .none) transition.setScale(layer: self.layer, scale: 1.0) self.layer.animateScale(from: currentScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] completed in @@ -97,7 +97,7 @@ final class KeyEmojiView: HighlightTrackingButton { } } - func update(isExpanded: Bool, transition: Transition) -> CGSize { + func update(isExpanded: Bool, transition: ComponentTransition) -> CGSize { let params = Params(isExpanded: isExpanded) if let currentLayout = self.currentLayout, currentLayout.params == params { return currentLayout.size @@ -108,7 +108,7 @@ final class KeyEmojiView: HighlightTrackingButton { return size } - private func update(params: Params, transition: Transition) -> CGSize { + private func update(params: Params, transition: ComponentTransition) -> CGSize { let itemSpacing: CGFloat = 3.0 var height: CGFloat = 0.0 @@ -131,7 +131,7 @@ final class KeyEmojiView: HighlightTrackingButton { } } -func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat, duration: Double, curve: Transition.Animation.Curve, reverse: Bool) -> [CGPoint] { +func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat, duration: Double, curve: ComponentTransition.Animation.Curve, reverse: Bool) -> [CGPoint] { let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation) let x1 = sourcePoint.x diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/NoticeView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/NoticeView.swift index d307040facf..1e8d7c0964a 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/NoticeView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/NoticeView.swift @@ -61,7 +61,7 @@ final class NoticeView: OverlayMaskContainerView { self.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) } - func update(icon: String, text: String, constrainedWidth: CGFloat, transition: Transition) -> CGSize { + func update(icon: String, text: String, constrainedWidth: CGFloat, transition: ComponentTransition) -> CGSize { let sideInset: CGFloat = 12.0 let verticalInset: CGFloat = 6.0 let iconSpacing: CGFloat = -3.0 diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallPictureInPictureView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallPictureInPictureView.swift index b5a52fd67fd..8973b2cc14e 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallPictureInPictureView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallPictureInPictureView.swift @@ -166,11 +166,11 @@ final class PrivateCallPictureInPictureView: UIView { let animationDuration = CATransaction.animationDuration() let timingFunction = CATransaction.animationTimingFunction() - let mappedTransition: Transition + let mappedTransition: ComponentTransition if self.sampleBufferView.bounds.isEmpty { mappedTransition = .immediate } else if animationDuration > 0.0 && !CATransaction.disableActions() { - let mappedCurve: Transition.Animation.Curve + let mappedCurve: ComponentTransition.Animation.Curve if let timingFunction { var controlPoint0: [Float] = [0.0, 0.0] var controlPoint1: [Float] = [0.0, 0.0] @@ -182,7 +182,7 @@ final class PrivateCallPictureInPictureView: UIView { } else { mappedCurve = .easeInOut } - mappedTransition = Transition(animation: .curve( + mappedTransition = ComponentTransition(animation: .curve( duration: animationDuration, curve: mappedCurve )) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift index ee5b9998909..7a4cce70278 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift @@ -5,10 +5,10 @@ import MetalPerformanceShaders import Accelerate import MetalEngine -final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject { - var internalData: MetalEngineSubjectInternalData? +public final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject { + public var internalData: MetalEngineSubjectInternalData? - let blurredLayer: MetalEngineSubjectLayer + public let blurredLayer: MetalEngineSubjectLayer final class BlurState: ComputeState { let computePipelineStateYUVBiPlanarToRGBA: MTLComputePipelineState @@ -77,36 +77,36 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject { } } - var video: VideoSource.Output? { + public var video: VideoSource.Output? { didSet { self.setNeedsUpdate() } } - var renderSpec: RenderLayerSpec? + public var renderSpec: RenderLayerSpec? private var rgbaTexture: PooledTexture? private var downscaledTexture: PooledTexture? private var blurredHorizontalTexture: PooledTexture? private var blurredVerticalTexture: PooledTexture? - override init() { + override public init() { self.blurredLayer = MetalEngineSubjectLayer() super.init() } - override init(layer: Any) { + override public init(layer: Any) { self.blurredLayer = MetalEngineSubjectLayer() super.init(layer: layer) } - required init?(coder: NSCoder) { + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func update(context: MetalEngineSubjectContext) { + public func update(context: MetalEngineSubjectContext) { if self.isHidden { return } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RatingView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RatingView.swift index 6b549343134..4443881a262 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RatingView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RatingView.swift @@ -55,7 +55,7 @@ final class RatingView: OverlayMaskContainerView { self.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) } - func update(text: String, constrainedWidth: CGFloat, transition: Transition) -> CGSize { + func update(text: String, constrainedWidth: CGFloat, transition: ComponentTransition) -> CGSize { let sideInset: CGFloat = 12.0 let verticalInset: CGFloat = 6.0 diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift index af64a8b1a0c..f6fe5c68c66 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift @@ -59,7 +59,7 @@ final class RoundedCornersView: UIImageView { self.layer.cornerRadius = 0.0 } - func update(cornerRadius: CGFloat, transition: Transition) { + func update(cornerRadius: CGFloat, transition: ComponentTransition) { if self.currentCornerRadius == cornerRadius { return } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift index 30f287c191f..26569c3b613 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift @@ -257,7 +257,7 @@ final class StatusView: UIView { self.activeDurationTimer?.invalidate() } - func update(strings: PresentationStrings, state: State, transition: Transition) -> CGSize { + func update(strings: PresentationStrings, state: State, transition: ComponentTransition) -> CGSize { if let layoutState = self.layoutState, layoutState.strings === strings, layoutState.state == state { return layoutState.size } @@ -302,7 +302,7 @@ final class StatusView: UIView { } } - private func updateInternal(strings: PresentationStrings, state: State, transition: Transition) -> CGSize { + private func updateInternal(strings: PresentationStrings, state: State, transition: ComponentTransition) -> CGSize { let textString: String var needsDots = false var monospacedDigits = false diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/TitleView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/TitleView.swift index 59a938b5783..9d94c24a1da 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/TitleView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/TitleView.swift @@ -43,7 +43,7 @@ final class TextView: UIView { return super.action(for: layer, forKey: event) } - func update(string: String, fontSize: CGFloat, fontWeight: CGFloat, monospacedDigits: Bool = false, alignment: NSTextAlignment = .natural, color: UIColor, constrainedWidth: CGFloat, transition: Transition) -> CGSize { + func update(string: String, fontSize: CGFloat, fontWeight: CGFloat, monospacedDigits: Bool = false, alignment: NSTextAlignment = .natural, color: UIColor, constrainedWidth: CGFloat, transition: ComponentTransition) -> CGSize { let params = Params(string: string, fontSize: fontSize, fontWeight: fontWeight, monospacedDigits: monospacedDigits, alignment: alignment, constrainedWidth: constrainedWidth) if let layoutState = self.layoutState, layoutState.params == params { return layoutState.size diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift index 4a55077b858..8a18633c710 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift @@ -50,7 +50,7 @@ private final class VideoContainerLayer: SimpleLayer { fatalError("init(coder:) has not been implemented") } - func update(size: CGSize, transition: Transition) { + func update(size: CGSize, transition: ComponentTransition) { transition.setFrame(layer: self.contentsLayer, frame: CGRect(origin: CGPoint(), size: size)) } } @@ -268,13 +268,13 @@ final class VideoContainerView: HighlightTrackingButton { if highlightedState { self.videoContainerLayer.removeAnimation(forKey: "sublayerTransform") - let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.15, curve: .easeInOut)) transition.setSublayerTransform(layer: self.videoContainerLayer, transform: CATransform3DMakeScale(topScale, topScale, 1.0)) } else { let t = self.videoContainerLayer.presentation()?.sublayerTransform ?? self.videoContainerLayer.sublayerTransform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) - let transition = Transition(animation: .none) + let transition = ComponentTransition(animation: .none) transition.setSublayerTransform(layer: self.videoContainerLayer, transform: CATransform3DIdentity) self.videoContainerLayer.animateSublayerScale(from: currentScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] completed in @@ -462,14 +462,14 @@ final class VideoContainerView: HighlightTrackingButton { self.dragPositionAnimatorLink = nil } - private func update(transition: Transition) { + private func update(transition: ComponentTransition) { guard let params = self.params else { return } self.update(previousParams: params, params: params, transition: transition) } - func update(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool, transition: Transition) { + func update(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool, transition: ComponentTransition) { let params = Params(size: size, insets: insets, interfaceOrientation: interfaceOrientation, cornerRadius: cornerRadius, controlsHidden: controlsHidden, isMinimized: isMinimized, isAnimatedOut: isAnimatedOut) if self.params == params { return @@ -548,7 +548,7 @@ final class VideoContainerView: HighlightTrackingButton { ) } - private func update(previousParams: Params?, params: Params, transition: Transition) { + private func update(previousParams: Params?, params: Params, transition: ComponentTransition) { guard let videoMetrics = self.videoMetrics else { return } @@ -613,7 +613,7 @@ final class VideoContainerView: HighlightTrackingButton { animateFlipDisappearingVideo = disappearingVideoLayer disappearingVideoLayer.videoLayer.blurredLayer.removeFromSuperlayer() } else { - let alphaTransition: Transition = .easeInOut(duration: 0.2) + let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) let disappearingVideoLayerValue = disappearingVideoLayer.videoLayer alphaTransition.setAlpha(layer: disappearingVideoLayerValue, alpha: 0.0, completion: { [weak self, weak disappearingVideoLayerValue] _ in guard let self, let disappearingVideoLayerValue else { @@ -758,7 +758,7 @@ final class VideoContainerView: HighlightTrackingButton { transition.setPosition(layer: disappearingVideoLayer.videoLayer, position: videoFrame.center) transition.setPosition(layer: disappearingVideoLayer.videoLayer.blurredLayer, position: videoFrame.center) - let alphaTransition: Transition = .easeInOut(duration: 0.2) + let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) let disappearingVideoLayerValue = disappearingVideoLayer.videoLayer alphaTransition.setAlpha(layer: disappearingVideoLayerValue, alpha: 0.0, completion: { [weak disappearingVideoLayerValue] _ in disappearingVideoLayerValue?.removeFromSuperlayer() diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoShadowsView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoShadowsView.swift index 11cec5dbdfc..30d6f34de8e 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoShadowsView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoShadowsView.swift @@ -12,6 +12,6 @@ final class VideoShadowsView: UIView { fatalError("init(coder:) has not been implemented") } - func update(size: CGSize, transition: Transition) { + func update(size: CGSize, transition: ComponentTransition) { } } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index 1ae4c05e381..28fcc277ad3 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -470,7 +470,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu } } - public func update(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, screenCornerRadius: CGFloat, state: State, transition: Transition) { + public func update(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, screenCornerRadius: CGFloat, state: State, transition: ComponentTransition) { let params = Params(size: size, insets: insets, interfaceOrientation: interfaceOrientation, screenCornerRadius: screenCornerRadius, state: state) if self.params == params { return @@ -576,20 +576,20 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu self.updateInternal(params: params, transition: transition) } - private func update(transition: Transition) { + private func update(transition: ComponentTransition) { guard let params = self.params else { return } self.updateInternal(params: params, transition: transition) } - private func updateInternal(params: Params, transition: Transition) { + private func updateInternal(params: Params, transition: ComponentTransition) { self.isUpdating = true defer { self.isUpdating = false } - let genericAlphaTransition: Transition + let genericAlphaTransition: ComponentTransition switch transition.animation { case .none: genericAlphaTransition = .immediate @@ -780,7 +780,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu if self.isEmojiKeyExpanded { let emojiExpandedInfoView: EmojiExpandedInfoView var emojiExpandedInfoTransition = transition - let alphaTransition: Transition + let alphaTransition: ComponentTransition if let current = self.emojiExpandedInfoView { emojiExpandedInfoView = current alphaTransition = genericAlphaTransition @@ -795,7 +795,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu emojiExpandedInfoView = EmojiExpandedInfoView(title: params.state.strings.Call_EncryptedAlertTitle, text: params.state.strings.Call_EncryptedAlertText(params.state.shortName).string) self.emojiExpandedInfoView = emojiExpandedInfoView emojiExpandedInfoView.alpha = 0.0 - Transition.immediate.setScale(view: emojiExpandedInfoView, scale: 0.5) + ComponentTransition.immediate.setScale(view: emojiExpandedInfoView, scale: 0.5) emojiExpandedInfoView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.1) if let emojiView = self.emojiView { self.insertSubview(emojiExpandedInfoView, belowSubview: emojiView) @@ -825,7 +825,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu if let emojiExpandedInfoView = self.emojiExpandedInfoView { self.emojiExpandedInfoView = nil - let alphaTransition: Transition + let alphaTransition: ComponentTransition if !genericAlphaTransition.animation.isImmediate { alphaTransition = genericAlphaTransition.withAnimation(.curve(duration: 0.1, curve: .easeInOut)) } else { @@ -1038,8 +1038,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu videoContainerView.blurredContainerLayer.bounds = self.avatarTransformLayer.bounds videoContainerView.blurredContainerLayer.opacity = 0.0 videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: .immediate) - Transition.immediate.setScale(view: videoContainerView, scale: self.currentAvatarAudioScale) - Transition.immediate.setScale(view: self.videoContainerBackgroundView, scale: self.currentAvatarAudioScale) + ComponentTransition.immediate.setScale(view: videoContainerView, scale: self.currentAvatarAudioScale) + ComponentTransition.immediate.setScale(view: self.videoContainerBackgroundView, scale: self.currentAvatarAudioScale) } else { videoContainerView.layer.position = expandedVideoFrame.center videoContainerView.layer.bounds = CGRect(origin: CGPoint(), size: expandedVideoFrame.size) @@ -1059,7 +1059,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu videoContainerTransition.setScale(layer: videoContainerView.blurredContainerLayer, scale: 1.0) videoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: false, transition: videoContainerTransition) - let alphaTransition: Transition + let alphaTransition: ComponentTransition switch transition.animation { case .none: alphaTransition = .immediate @@ -1091,7 +1091,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu removedVideoContainerIndices.append(i) if self.videoContainerViews.count == 1 || (i == 0 && !havePrimaryVideo) { - let alphaTransition: Transition = genericAlphaTransition + let alphaTransition: ComponentTransition = genericAlphaTransition videoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: avatarCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: transition) transition.setPosition(layer: videoContainerView.blurredContainerLayer, position: avatarFrame.center) @@ -1284,7 +1284,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu if !transition.animation.isImmediate { transition.setPosition(view: previousStatusView, position: CGPoint(x: previousStatusView.center.x, y: previousStatusView.center.y - 5.0)) transition.setScale(view: previousStatusView, scale: 0.5) - Transition.easeInOut(duration: 0.1).setAlpha(view: previousStatusView, alpha: 0.0, completion: { [weak previousStatusView] _ in + ComponentTransition.easeInOut(duration: 0.1).setAlpha(view: previousStatusView, alpha: 0.0, completion: { [weak previousStatusView] _ in previousStatusView?.removeFromSuperview() }) } else { @@ -1331,7 +1331,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu if !transition.animation.isImmediate { transition.animatePosition(view: self.statusView, from: CGPoint(x: 0.0, y: 5.0), to: CGPoint(), additive: true) transition.animateScale(view: self.statusView, from: 0.5, to: 1.0) - Transition.easeInOut(duration: 0.15).animateAlpha(view: self.statusView, from: 0.0, to: 1.0) + ComponentTransition.easeInOut(duration: 0.15).animateAlpha(view: self.statusView, from: 0.0, to: 1.0) } } else { transition.setFrame(view: self.statusView, frame: statusFrame) @@ -1358,7 +1358,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu if weakSignalView.bounds.isEmpty { weakSignalView.frame = weakSignalFrame if !transition.animation.isImmediate { - Transition.immediate.setScale(view: weakSignalView, scale: 0.001) + ComponentTransition.immediate.setScale(view: weakSignalView, scale: 0.001) weakSignalView.alpha = 0.0 transition.setScaleWithSpring(view: weakSignalView, scale: 1.0) transition.setAlpha(view: weakSignalView, alpha: 1.0) diff --git a/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/BUILD b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/BUILD new file mode 100644 index 00000000000..6720eda7209 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/BUILD @@ -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 = "VoiceChatActionButtonMetalSources", + srcs = glob([ + "Metal/**/*.metal", + ]), + visibility = ["//visibility:public"], +) + +plist_fragment( + name = "VoiceChatActionButtonMetalSourcesBundleInfoPlist", + extension = "plist", + template = + """ + CFBundleIdentifier + org.telegram.VoiceChatActionButtonMetalSources + CFBundleDevelopmentRegion + en + CFBundleName + VoiceChatActionButton + """ +) + +apple_resource_bundle( + name = "VoiceChatActionButtonMetalSourcesBundle", + infoplists = [ + ":VoiceChatActionButtonMetalSourcesBundleInfoPlist", + ], + resources = [ + ":VoiceChatActionButtonMetalSources", + ], +) + +filegroup( + name = "Assets", + srcs = glob(["VoiceChatActionButtonAssets.xcassets/**"]), + visibility = ["//visibility:public"], +) + +swift_library( + name = "VoiceChatActionButton", + module_name = "VoiceChatActionButton", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + data = [ + ":VoiceChatActionButtonMetalSourcesBundle", + ":Assets", + ], + deps = [ + "//submodules/Display", + "//submodules/MetalEngine", + "//submodules/ComponentFlow", + "//submodules/UIKitRuntimeUtils", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AppBundle", + "//submodules/ManagedAnimationNode", + "//submodules/AnimationUI", + "//submodules/TelegramUI/Components/Calls/CallScreen", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Metal/VoiceChatActionButtonShaders.metal b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Metal/VoiceChatActionButtonShaders.metal new file mode 100644 index 00000000000..8834ab1672c --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Metal/VoiceChatActionButtonShaders.metal @@ -0,0 +1,22 @@ +#include + +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; +}; diff --git a/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/BlobView.swift b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/BlobView.swift new file mode 100644 index 00000000000..faffd79a986 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/BlobView.swift @@ -0,0 +1,168 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display + +final class BlobView: UIView { + let pointsCount: Int + let smoothness: CGFloat + + let minRandomness: CGFloat + let maxRandomness: CGFloat + + let minSpeed: CGFloat + let maxSpeed: CGFloat + + let minScale: CGFloat + let maxScale: CGFloat + + var scaleUpdated: ((CGFloat) -> Void)? + + var level: CGFloat = 0 { + didSet { + if abs(self.level - oldValue) > 0.01 { + CATransaction.begin() + CATransaction.setDisableActions(true) + let lv = self.minScale + (self.maxScale - self.minScale) * self.level + self.shapeLayer.transform = CATransform3DMakeScale(lv, lv, 1) + self.scaleUpdated?(self.level) + CATransaction.commit() + } + } + } + + private var speedLevel: CGFloat = 0 + private var lastSpeedLevel: CGFloat = 0 + + private let shapeLayer: CAShapeLayer = { + let layer = CAShapeLayer() + layer.strokeColor = nil + return layer + }() + + init( + pointsCount: Int, + minRandomness: CGFloat, + maxRandomness: CGFloat, + minSpeed: CGFloat, + maxSpeed: CGFloat, + minScale: CGFloat, + maxScale: CGFloat + ) { + self.pointsCount = pointsCount + self.minRandomness = minRandomness + self.maxRandomness = maxRandomness + self.minSpeed = minSpeed + self.maxSpeed = maxSpeed + self.minScale = minScale + self.maxScale = maxScale + + let angle = (CGFloat.pi * 2) / CGFloat(pointsCount) + self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2 + + super.init(frame: .zero) + + self.layer.addSublayer(self.shapeLayer) + + self.shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setColor(_ color: UIColor) { + self.shapeLayer.fillColor = color.cgColor + } + + func updateSpeedLevel(to newSpeedLevel: CGFloat) { + self.speedLevel = max(self.speedLevel, newSpeedLevel) + } + + func startAnimating() { + self.animateToNewShape() + } + + func stopAnimating() { + self.shapeLayer.removeAnimation(forKey: "path") + } + + private func animateToNewShape() { + if self.shapeLayer.path == nil { + let points = generateNextBlob(for: self.bounds.size) + self.shapeLayer.path = UIBezierPath.smoothCurve(through: points, length: bounds.width, smoothness: smoothness).cgPath + } + + let nextPoints = generateNextBlob(for: self.bounds.size) + let nextPath = UIBezierPath.smoothCurve(through: nextPoints, length: bounds.width, smoothness: smoothness).cgPath + + let animation = CABasicAnimation(keyPath: "path") + let previousPath = self.shapeLayer.path + self.shapeLayer.path = nextPath + animation.duration = CFTimeInterval(1.0 / (minSpeed + (maxSpeed - minSpeed) * speedLevel)) + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + animation.fromValue = previousPath + animation.toValue = nextPath + animation.isRemovedOnCompletion = false + animation.fillMode = .forwards + animation.completion = { [weak self] finished in + if finished { + self?.animateToNewShape() + } + } + + self.shapeLayer.add(animation, forKey: "path") + + self.lastSpeedLevel = self.speedLevel + self.speedLevel = 0 + } + + // MARK: Helpers + + private func generateNextBlob(for size: CGSize) -> [CGPoint] { + let randomness = minRandomness + (maxRandomness - minRandomness) * speedLevel + return blob(pointsCount: pointsCount, randomness: randomness) + .map { + return CGPoint( + x: $0.x * CGFloat(size.width), + y: $0.y * CGFloat(size.height) + ) + } + } + + func blob(pointsCount: Int, randomness: CGFloat) -> [CGPoint] { + let angle = (CGFloat.pi * 2) / CGFloat(pointsCount) + + let rgen = { () -> CGFloat in + let accuracy: UInt32 = 1000 + let random = arc4random_uniform(accuracy) + return CGFloat(random) / CGFloat(accuracy) + } + let rangeStart: CGFloat = 1 / (1 + randomness / 10) + + let startAngle = angle * CGFloat(arc4random_uniform(100)) / CGFloat(100) + + let points = (0 ..< pointsCount).map { i -> CGPoint in + let randPointOffset = (rangeStart + CGFloat(rgen()) * (1 - rangeStart)) / 2 + let angleRandomness: CGFloat = angle * 0.1 + let randAngle = angle + angle * ((angleRandomness * CGFloat(arc4random_uniform(100)) / CGFloat(100)) - angleRandomness * 0.5) + let pointX = sin(startAngle + CGFloat(i) * randAngle) + let pointY = cos(startAngle + CGFloat(i) * randAngle) + return CGPoint( + x: pointX * randPointOffset, + y: pointY * randPointOffset + ) + } + + return points + } + + override func layoutSubviews() { + super.layoutSubviews() + + CATransaction.begin() + CATransaction.setDisableActions(true) + self.shapeLayer.position = CGPoint(x: bounds.midX, y: bounds.midY) + CATransaction.commit() + } +} diff --git a/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceBlobView.swift b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceBlobView.swift new file mode 100644 index 00000000000..60a60e0fb9e --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceBlobView.swift @@ -0,0 +1,174 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import CallScreen +import MetalEngine + +final class VoiceBlobView: UIView { + //private let mediumBlob: BlobView + //private let bigBlob: BlobView + + private let blobsLayer: CallBlobsLayer + + private let maxLevel: CGFloat + + private var displayLinkAnimator: ConstantDisplayLinkAnimator? + + private var audioLevel: CGFloat = 0.0 + var presentationAudioLevel: CGFloat = 0.0 + + var scaleUpdated: ((CGFloat) -> Void)? { + didSet { + //self.bigBlob.scaleUpdated = self.scaleUpdated + } + } + + private(set) var isAnimating = false + + public typealias BlobRange = (min: CGFloat, max: CGFloat) + + private let hierarchyTrackingNode: HierarchyTrackingNode + private var isCurrentlyInHierarchy = true + + init( + frame: CGRect, + maxLevel: CGFloat, + mediumBlobRange: BlobRange, + bigBlobRange: BlobRange + ) { + var updateInHierarchy: ((Bool) -> Void)? + self.hierarchyTrackingNode = HierarchyTrackingNode({ value in + updateInHierarchy?(value) + }) + + self.maxLevel = maxLevel + + /*self.mediumBlob = BlobView( + pointsCount: 8, + minRandomness: 1, + maxRandomness: 1, + minSpeed: 0.9, + maxSpeed: 4.0, + minScale: mediumBlobRange.min, + maxScale: mediumBlobRange.max + ) + self.bigBlob = BlobView( + pointsCount: 8, + minRandomness: 1, + maxRandomness: 1, + minSpeed: 1.0, + maxSpeed: 4.4, + minScale: bigBlobRange.min, + maxScale: bigBlobRange.max + )*/ + + self.blobsLayer = CallBlobsLayer() + + super.init(frame: frame) + + self.addSubnode(self.hierarchyTrackingNode) + + //self.addSubview(self.bigBlob) + //self.addSubview(self.mediumBlob) + self.layer.addSublayer(self.blobsLayer) + + self.displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in + guard let strongSelf = self else { return } + + if !strongSelf.isCurrentlyInHierarchy { + return + } + + strongSelf.presentationAudioLevel = strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1 + strongSelf.updateAudioLevel() + + //strongSelf.mediumBlob.level = strongSelf.presentationAudioLevel + //strongSelf.bigBlob.level = strongSelf.presentationAudioLevel + } + + updateInHierarchy = { [weak self] value in + if let strongSelf = self { + strongSelf.isCurrentlyInHierarchy = value + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func setColor(_ color: UIColor) { + //self.mediumBlob.setColor(color.withAlphaComponent(0.5)) + //self.bigBlob.setColor(color.withAlphaComponent(0.21)) + } + + public func updateLevel(_ level: CGFloat, immediately: Bool) { + let normalizedLevel = min(1, max(level / maxLevel, 0)) + + //self.mediumBlob.updateSpeedLevel(to: normalizedLevel) + //self.bigBlob.updateSpeedLevel(to: normalizedLevel) + + self.audioLevel = normalizedLevel + if immediately { + self.presentationAudioLevel = normalizedLevel + } + } + + private func updateAudioLevel() { + let additionalAvatarScale = CGFloat(max(0.0, min(self.presentationAudioLevel * 18.0, 5.0)) * 0.05) + let blobAmplificationFactor: CGFloat = 2.0 + let blobScale = 1.0 + additionalAvatarScale * blobAmplificationFactor + self.blobsLayer.transform = CATransform3DMakeScale(blobScale, blobScale, 1.0) + + self.scaleUpdated?(blobScale) + } + + public func startAnimating() { + guard !self.isAnimating else { return } + self.isAnimating = true + + self.updateBlobsState() + + self.displayLinkAnimator?.isPaused = false + } + + public func stopAnimating() { + self.stopAnimating(duration: 0.15) + } + + public func stopAnimating(duration: Double) { + guard isAnimating else { return } + self.isAnimating = false + + self.updateBlobsState() + + self.displayLinkAnimator?.isPaused = true + } + + private func updateBlobsState() { + /*if self.isAnimating { + if self.mediumBlob.frame.size != .zero { + self.mediumBlob.startAnimating() + self.bigBlob.startAnimating() + } + } else { + self.mediumBlob.stopAnimating() + self.bigBlob.stopAnimating() + }*/ + } + + override public func layoutSubviews() { + super.layoutSubviews() + + //self.mediumBlob.frame = bounds + //self.bigBlob.frame = bounds + + let blobsFrame = bounds.insetBy(dx: floor(bounds.width * 0.12), dy: floor(bounds.height * 0.12)) + self.blobsLayer.position = blobsFrame.center + self.blobsLayer.bounds = CGRect(origin: CGPoint(), size: blobsFrame.size) + + self.updateBlobsState() + } +} + diff --git a/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceChatActionButton.swift b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceChatActionButton.swift new file mode 100644 index 00000000000..48f73b803f6 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceChatActionButton.swift @@ -0,0 +1,521 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import AnimationUI +import AppBundle +import ManagedAnimationNode +import ComponentFlow + +private let titleFont = Font.regular(15.0) +private let subtitleFont = Font.regular(13.0) + +private let smallScale: CGFloat = 0.48 +private let smallIconScale: CGFloat = 0.69 + +public final class VoiceChatActionButton: HighlightTrackingButtonNode { + static let buttonHeight: CGFloat = 52.0 + + public enum State: Equatable { + public enum ActiveState: Equatable { + case cantSpeak + case muted + case on + } + + public enum ScheduledState: Equatable { + case start + case subscribe + case unsubscribe + } + + case button(text: String) + case scheduled(state: ScheduledState) + case connecting + case active(state: ActiveState) + } + + public var stateValue: State { + return self.currentParams?.state ?? .connecting + } + public var statePromise = ValuePromise() + public var state: Signal { + return self.statePromise.get() + } + + public let bottomNode: ASDisplayNode + private let containerNode: ASDisplayNode + private let backgroundNode: VoiceChatActionButtonBackgroundNode + private let iconNode: VoiceChatActionButtonIconNode + private let labelContainerNode: ASDisplayNode + public let titleLabel: ImmediateTextNode + private let subtitleLabel: ImmediateTextNode + private let buttonTitleLabel: ImmediateTextNode + + private var currentParams: (size: CGSize, buttonSize: CGSize, state: VoiceChatActionButton.State, dark: Bool, small: Bool, title: String, subtitle: String, snap: Bool)? + + private var activePromise = ValuePromise(false) + private var outerColorPromise = Promise<(UIColor?, UIColor?)>((nil, nil)) + public var outerColor: Signal<(UIColor?, UIColor?), NoError> { + return self.outerColorPromise.get() + } + + public var connectingColor: UIColor = UIColor(rgb: 0xb6b6bb) { + didSet { + self.backgroundNode.connectingColor = self.connectingColor + } + } + + public var activeDisposable = MetaDisposable() + + public var isDisabled: Bool = false + + public var ignoreHierarchyChanges: Bool { + get { + return self.backgroundNode.ignoreHierarchyChanges + } set { + self.backgroundNode.ignoreHierarchyChanges = newValue + } + } + + public var wasActiveWhenPressed = false + public var pressing: Bool = false { + didSet { + guard let (_, _, state, _, small, _, _, snap) = self.currentParams, !self.isDisabled else { + return + } + if self.pressing { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) + if small { + transition.updateTransformScale(node: self.backgroundNode, scale: smallScale * 0.9) + transition.updateTransformScale(node: self.iconNode, scale: smallIconScale * 0.9) + } else { + transition.updateTransformScale(node: self.iconNode, scale: snap ? 0.5 : 0.9) + } + + switch state { + case let .active(state): + switch state { + case .on: + self.wasActiveWhenPressed = true + default: + break + } + case .connecting, .button, .scheduled: + break + } + } else { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) + if small { + transition.updateTransformScale(node: self.backgroundNode, scale: smallScale) + transition.updateTransformScale(node: self.iconNode, scale: smallIconScale) + } else { + transition.updateTransformScale(node: self.iconNode, scale: snap ? 0.5 : 1.0) + } + self.wasActiveWhenPressed = false + } + } + } + + public var animationsEnabled: Bool = true { + didSet { + self.backgroundNode.animationsEnabled = self.animationsEnabled + } + } + + public init() { + self.bottomNode = ASDisplayNode() + self.bottomNode.isUserInteractionEnabled = false + self.containerNode = ASDisplayNode() + self.containerNode.isUserInteractionEnabled = false + self.backgroundNode = VoiceChatActionButtonBackgroundNode() + self.iconNode = VoiceChatActionButtonIconNode(isColored: false) + + self.labelContainerNode = ASDisplayNode() + self.titleLabel = ImmediateTextNode() + self.subtitleLabel = ImmediateTextNode() + self.buttonTitleLabel = ImmediateTextNode() + self.buttonTitleLabel.isUserInteractionEnabled = false + self.buttonTitleLabel.alpha = 0.0 + + super.init() + + self.addSubnode(self.bottomNode) + self.labelContainerNode.addSubnode(self.titleLabel) + self.labelContainerNode.addSubnode(self.subtitleLabel) + self.addSubnode(self.labelContainerNode) + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.backgroundNode) + self.containerNode.addSubnode(self.iconNode) + + self.containerNode.addSubnode(self.buttonTitleLabel) + + self.highligthedChanged = { [weak self] pressing in + if let strongSelf = self { + guard let (_, _, state, _, small, _, _, snap) = strongSelf.currentParams else { + return + } + if pressing { + if case .button = state { + strongSelf.containerNode.layer.removeAnimation(forKey: "opacity") + strongSelf.containerNode.alpha = 0.4 + } else { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) + if small { + transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale * 0.9) + transition.updateTransformScale(node: strongSelf.iconNode, scale: smallIconScale * 0.9) + } else { + transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 0.9) + } + } + } else if !strongSelf.pressing { + if case .button = state { + strongSelf.containerNode.alpha = 1.0 + strongSelf.containerNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } else { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) + if small { + transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale) + transition.updateTransformScale(node: strongSelf.iconNode, scale: smallIconScale) + } else { + transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 1.0) + } + } + } + } + } + + self.backgroundNode.updatedActive = { [weak self] active in + self?.activePromise.set(active) + } + + self.backgroundNode.updatedColors = { [weak self] outerColor, activeColor in + self?.outerColorPromise.set(.single((outerColor, activeColor))) + } + } + + deinit { + self.activeDisposable.dispose() + } + + public func updateLevel(_ level: CGFloat, immediately: Bool = false) { + self.backgroundNode.audioLevel = level + } + + private func applyParams(animated: Bool) { + guard let (size, _, state, _, small, title, subtitle, snap) = self.currentParams else { + return + } + + let updatedTitle = self.titleLabel.attributedText?.string != title + let updatedSubtitle = self.subtitleLabel.attributedText?.string != subtitle + + self.titleLabel.attributedText = NSAttributedString(string: title, font: titleFont, textColor: .white) + self.subtitleLabel.attributedText = NSAttributedString(string: subtitle, font: subtitleFont, textColor: .white) + + if animated && self.titleLabel.alpha > 0.0 { + if let snapshotView = self.titleLabel.view.snapshotContentTree(), updatedTitle { + self.titleLabel.view.superview?.insertSubview(snapshotView, belowSubview: self.titleLabel.view) + snapshotView.frame = self.titleLabel.frame + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + self.titleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + if let snapshotView = self.subtitleLabel.view.snapshotContentTree(), updatedSubtitle { + self.subtitleLabel.view.superview?.insertSubview(snapshotView, belowSubview: self.subtitleLabel.view) + snapshotView.frame = self.subtitleLabel.frame + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + self.subtitleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + } + + let titleSize = self.titleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude)) + let subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude)) + let totalHeight = titleSize.height + subtitleSize.height + 1.0 + + self.labelContainerNode.frame = CGRect(origin: CGPoint(), size: size) + + let titleLabelFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - totalHeight) / 2.0) + 84.0), size: titleSize) + let subtitleLabelFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: titleLabelFrame.maxY + 1.0), size: subtitleSize) + + self.titleLabel.bounds = CGRect(origin: CGPoint(), size: titleLabelFrame.size) + self.titleLabel.position = titleLabelFrame.center + self.subtitleLabel.bounds = CGRect(origin: CGPoint(), size: subtitleLabelFrame.size) + self.subtitleLabel.position = subtitleLabelFrame.center + + self.bottomNode.frame = CGRect(origin: CGPoint(), size: size) + self.containerNode.frame = CGRect(origin: CGPoint(), size: size) + + self.backgroundNode.bounds = CGRect(origin: CGPoint(), size: size) + self.backgroundNode.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + + var active = false + switch state { + case let .active(state): + switch state { + case .on: + active = self.pressing && !self.wasActiveWhenPressed + default: + break + } + case .connecting, .button, .scheduled: + break + } + + if snap { + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate + transition.updateTransformScale(node: self.backgroundNode, scale: active ? 0.9 : 0.625) + transition.updateTransformScale(node: self.iconNode, scale: 0.625) + transition.updateAlpha(node: self.titleLabel, alpha: 0.0) + transition.updateAlpha(node: self.subtitleLabel, alpha: 0.0) + transition.updateAlpha(layer: self.backgroundNode.maskProgressLayer, alpha: 0.0) + } else { + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate + if small { + transition.updateTransformScale(node: self.backgroundNode, scale: self.pressing ? smallScale * 0.9 : smallScale, delay: 0.0) + transition.updateTransformScale(node: self.iconNode, scale: self.pressing ? smallIconScale * 0.9 : smallIconScale, delay: 0.0) + transition.updateAlpha(node: self.titleLabel, alpha: 0.0) + transition.updateAlpha(node: self.subtitleLabel, alpha: 0.0) + transition.updateSublayerTransformOffset(layer: self.labelContainerNode.layer, offset: CGPoint(x: 0.0, y: -43.0)) + transition.updateTransformScale(node: self.titleLabel, scale: 0.8) + transition.updateTransformScale(node: self.subtitleLabel, scale: 0.8) + } else { + transition.updateTransformScale(node: self.backgroundNode, scale: 1.0, delay: 0.0) + transition.updateTransformScale(node: self.iconNode, scale: self.pressing ? 0.9 : 1.0, delay: 0.0) + transition.updateAlpha(node: self.titleLabel, alpha: 1.0, delay: 0.05) + transition.updateAlpha(node: self.subtitleLabel, alpha: 1.0, delay: 0.05) + transition.updateSublayerTransformOffset(layer: self.labelContainerNode.layer, offset: CGPoint()) + transition.updateTransformScale(node: self.titleLabel, scale: 1.0) + transition.updateTransformScale(node: self.subtitleLabel, scale: 1.0) + } + transition.updateAlpha(layer: self.backgroundNode.maskProgressLayer, alpha: 1.0) + } + + let iconSize = CGSize(width: 100.0, height: 100.0) + self.iconNode.bounds = CGRect(origin: CGPoint(), size: iconSize) + self.iconNode.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + } + + private var previousIcon: VoiceChatActionButtonIconAnimationState? + private func applyIconParams() { + guard let (_, _, state, _, _, _, _, _) = self.currentParams else { + return + } + + let icon: VoiceChatActionButtonIconAnimationState + switch state { + case .button: + icon = .empty + case let .scheduled(state): + switch state { + case .start: + icon = .start + case .subscribe: + icon = .subscribe + case .unsubscribe: + icon = .unsubscribe + } + case let .active(state): + switch state { + case .on: + icon = .unmute + case .muted: + icon = .mute + case .cantSpeak: + icon = .hand + } + case .connecting: + if let previousIcon = previousIcon { + icon = previousIcon + } else { + icon = .mute + } + } + self.previousIcon = icon + + self.iconNode.enqueueState(icon) + } + + public func update(snap: Bool, animated: Bool) { + if let previous = self.currentParams { + self.currentParams = (previous.size, previous.buttonSize, previous.state, previous.dark, previous.small, previous.title, previous.subtitle, snap) + + self.backgroundNode.isSnap = snap + self.backgroundNode.glowHidden = snap || previous.small + self.backgroundNode.updateColors() + self.applyParams(animated: animated) + self.applyIconParams() + } + } + + public func update(size: CGSize, buttonSize: CGSize, state: VoiceChatActionButton.State, title: String, subtitle: String, dark: Bool, small: Bool, animated: Bool = false) { + let previous = self.currentParams + let previousState = previous?.state + self.currentParams = (size, buttonSize, state, dark, small, title, subtitle, previous?.snap ?? false) + + self.statePromise.set(state) + + if let previousState = previousState, case .button = previousState, case .scheduled = state { + self.buttonTitleLabel.alpha = 0.0 + self.buttonTitleLabel.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.buttonTitleLabel.layer.animateScale(from: 1.0, to: 0.001, duration: 0.24) + + self.iconNode.alpha = 1.0 + self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.iconNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0) + } + + var backgroundState: VoiceChatActionButtonBackgroundNode.State + var animated = true + switch state { + case let .button(text): + backgroundState = .button + self.buttonTitleLabel.alpha = 1.0 + self.buttonTitleLabel.attributedText = NSAttributedString(string: text, font: Font.semibold(17.0), textColor: .white) + let titleSize = self.buttonTitleLabel.updateLayout(CGSize(width: size.width, height: 100.0)) + self.buttonTitleLabel.frame = CGRect(origin: CGPoint(x: floor((self.bounds.width - titleSize.width) / 2.0), y: floor((self.bounds.height - titleSize.height) / 2.0)), size: titleSize) + case .scheduled: + backgroundState = .disabled + if previousState == .connecting { + animated = false + } + case let .active(state): + switch state { + case .on: + backgroundState = .blob(true) + case .muted: + backgroundState = .blob(false) + case .cantSpeak: + backgroundState = .disabled + } + case .connecting: + backgroundState = .connecting + } + self.applyIconParams() + + self.backgroundNode.glowHidden = (self.currentParams?.snap ?? false) || small + self.backgroundNode.isDark = dark + self.backgroundNode.update(state: backgroundState, animated: animated) + + if case .active = state, let previousState = previousState, case .connecting = previousState, animated { + self.activeDisposable.set((self.activePromise.get() + |> deliverOnMainQueue).start(next: { [weak self] active in + if active { + self?.activeDisposable.set(nil) + self?.applyParams(animated: true) + } + })) + } else { + self.applyParams(animated: animated) + } + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + var hitRect = self.bounds + if let (_, buttonSize, state, _, _, _, _, _) = self.currentParams { + if case .button = state { + hitRect = CGRect(x: 0.0, y: floor((self.bounds.height - VoiceChatActionButton.buttonHeight) / 2.0), width: self.bounds.width, height: VoiceChatActionButton.buttonHeight) + } else { + hitRect = self.bounds.insetBy(dx: (self.bounds.width - buttonSize.width) / 2.0, dy: (self.bounds.height - buttonSize.height) / 2.0) + } + } + let result = super.hitTest(point, with: event) + if !hitRect.contains(point) { + return nil + } + return result + } + + public func playAnimation() { + self.iconNode.playRandomAnimation() + } +} + +public extension UIBezierPath { + static func smoothCurve(through points: [CGPoint], length: CGFloat, smoothness: CGFloat, curve: Bool = false) -> UIBezierPath { + var smoothPoints = [SmoothPoint]() + for index in (0 ..< points.count) { + let prevIndex = index - 1 + let prev = points[prevIndex >= 0 ? prevIndex : points.count + prevIndex] + let curr = points[index] + let next = points[(index + 1) % points.count] + + let angle: CGFloat = { + let dx = next.x - prev.x + let dy = -next.y + prev.y + let angle = atan2(dy, dx) + if angle < 0 { + return abs(angle) + } else { + return 2 * .pi - angle + } + }() + + smoothPoints.append( + SmoothPoint( + point: curr, + inAngle: angle + .pi, + inLength: smoothness * distance(from: curr, to: prev), + outAngle: angle, + outLength: smoothness * distance(from: curr, to: next) + ) + ) + } + + let resultPath = UIBezierPath() + if curve { + resultPath.move(to: CGPoint()) + resultPath.addLine(to: smoothPoints[0].point) + } else { + resultPath.move(to: smoothPoints[0].point) + } + + let smoothCount = curve ? smoothPoints.count - 1 : smoothPoints.count + for index in (0 ..< smoothCount) { + let curr = smoothPoints[index] + let next = smoothPoints[(index + 1) % points.count] + let currSmoothOut = curr.smoothOut() + let nextSmoothIn = next.smoothIn() + resultPath.addCurve(to: next.point, controlPoint1: currSmoothOut, controlPoint2: nextSmoothIn) + } + if curve { + resultPath.addLine(to: CGPoint(x: length, y: 0.0)) + } + resultPath.close() + return resultPath + } + + static private func distance(from fromPoint: CGPoint, to toPoint: CGPoint) -> CGFloat { + return sqrt((fromPoint.x - toPoint.x) * (fromPoint.x - toPoint.x) + (fromPoint.y - toPoint.y) * (fromPoint.y - toPoint.y)) + } + + struct SmoothPoint { + let point: CGPoint + + let inAngle: CGFloat + let inLength: CGFloat + + let outAngle: CGFloat + let outLength: CGFloat + + func smoothIn() -> CGPoint { + return smooth(angle: inAngle, length: inLength) + } + + func smoothOut() -> CGPoint { + return smooth(angle: outAngle, length: outLength) + } + + private func smooth(angle: CGFloat, length: CGFloat) -> CGPoint { + return CGPoint( + x: point.x + length * cos(angle), + y: point.y + length * sin(angle) + ) + } + } +} diff --git a/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceChatActionButtonBackgroundNode.swift b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceChatActionButtonBackgroundNode.swift new file mode 100644 index 00000000000..aa081666dbd --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceChatActionButtonBackgroundNode.swift @@ -0,0 +1,744 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display + +private let progressLineWidth: CGFloat = 3.0 + UIScreenPixel +private let buttonSize = CGSize(width: 112.0, height: 112.0) +private let radius = buttonSize.width / 2.0 + +private let areaSize = CGSize(width: 300.0, height: 300.0) +private let blobSize = CGSize(width: 190.0, height: 190.0) + +private let secondaryGreyColor = UIColor(rgb: 0x1c1c1e) +private let whiteColor = UIColor(rgb: 0xffffff) +private let greyColor = UIColor(rgb: 0x2c2c2e) +private let blue = UIColor(rgb: 0x007fff) +private let lightBlue = UIColor(rgb: 0x00affe) +private let green = UIColor(rgb: 0x33c659) +private let activeBlue = UIColor(rgb: 0x00a0b9) +private let purple = UIColor(rgb: 0x3252ef) +private let pink = UIColor(rgb: 0xef436c) + +final class VoiceChatActionButtonBackgroundNode: ASDisplayNode { + enum State: Equatable { + case connecting + case disabled + case button + case blob(Bool) + } + + private var state: State + private var hasState = false + + private var transition: State? + + var audioLevel: CGFloat = 0.0 { + didSet { + self.maskBlobView.updateLevel(self.audioLevel, immediately: false) + } + } + + var updatedActive: ((Bool) -> Void)? + var updatedColors: ((UIColor?, UIColor?) -> Void)? + + private let backgroundCircleLayer = CAShapeLayer() + private let foregroundCircleLayer = CAShapeLayer() + private let growingForegroundCircleLayer = CAShapeLayer() + + private let foregroundView = UIView() + private let foregroundGradientLayer = CAGradientLayer() + + private let maskView = UIView() + private let maskGradientLayer = CAGradientLayer() + private let maskBlobView: VoiceBlobView + private let maskCircleLayer = CAShapeLayer() + + let maskProgressLayer = CAShapeLayer() + + private let maskMediumBlobLayer = CAShapeLayer() + private let maskBigBlobLayer = CAShapeLayer() + + private let hierarchyTrackingNode: HierarchyTrackingNode + private var isCurrentlyInHierarchy = false + var ignoreHierarchyChanges = false + + override init() { + self.state = .connecting + + self.maskBlobView = VoiceBlobView(frame: CGRect(origin: CGPoint(x: (areaSize.width - blobSize.width) / 2.0, y: (areaSize.height - blobSize.height) / 2.0), size: blobSize), maxLevel: 1.5, mediumBlobRange: (0.69, 0.87), bigBlobRange: (0.71, 1.0)) + self.maskBlobView.setColor(whiteColor) + self.maskBlobView.isHidden = true + + var updateInHierarchy: ((Bool) -> Void)? + self.hierarchyTrackingNode = HierarchyTrackingNode({ value in + updateInHierarchy?(value) + }) + + super.init() + + self.addSubnode(self.hierarchyTrackingNode) + + let circlePath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: buttonSize)).cgPath + self.backgroundCircleLayer.fillColor = greyColor.cgColor + self.backgroundCircleLayer.path = circlePath + + let smallerCirclePath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: CGSize(width: buttonSize.width - progressLineWidth, height: buttonSize.height - progressLineWidth))).cgPath + self.foregroundCircleLayer.fillColor = greyColor.cgColor + self.foregroundCircleLayer.path = smallerCirclePath + self.foregroundCircleLayer.transform = CATransform3DMakeScale(0.0, 0.0, 1) + self.foregroundCircleLayer.isHidden = true + + self.growingForegroundCircleLayer.fillColor = greyColor.cgColor + self.growingForegroundCircleLayer.path = smallerCirclePath + self.growingForegroundCircleLayer.transform = CATransform3DMakeScale(1.0, 1.0, 1) + self.growingForegroundCircleLayer.isHidden = true + + self.foregroundGradientLayer.type = .radial + self.foregroundGradientLayer.colors = [lightBlue.cgColor, blue.cgColor, blue.cgColor] + self.foregroundGradientLayer.locations = [0.0, 0.55, 1.0] + self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0) + self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0) + + self.maskView.backgroundColor = .clear + + self.maskGradientLayer.type = .radial + self.maskGradientLayer.colors = [UIColor(rgb: 0xffffff, alpha: 0.4).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor] + self.maskGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5) + self.maskGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0) + self.maskGradientLayer.transform = CATransform3DMakeScale(0.3, 0.3, 1.0) + self.maskGradientLayer.isHidden = true + + let path = CGMutablePath() + path.addArc(center: CGPoint(x: (buttonSize.width + 6.0) / 2.0, y: (buttonSize.height + 6.0) / 2.0), radius: radius, startAngle: 0.0, endAngle: CGFloat.pi * 2.0, clockwise: true) + + self.maskProgressLayer.strokeColor = whiteColor.cgColor + self.maskProgressLayer.fillColor = UIColor.clear.cgColor + self.maskProgressLayer.lineWidth = progressLineWidth + self.maskProgressLayer.lineCap = .round + self.maskProgressLayer.path = path + + let circleFrame = CGRect(origin: CGPoint(x: (areaSize.width - buttonSize.width) / 2.0, y: (areaSize.height - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0) + let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath + + self.maskCircleLayer.path = largerCirclePath + self.maskCircleLayer.fillColor = whiteColor.cgColor + self.maskCircleLayer.isHidden = true + + updateInHierarchy = { [weak self] value in + if let strongSelf = self, !strongSelf.ignoreHierarchyChanges { + strongSelf.isCurrentlyInHierarchy = value + strongSelf.updateAnimations() + } + } + } + + override func didLoad() { + super.didLoad() + + self.layer.addSublayer(self.backgroundCircleLayer) + + self.view.addSubview(self.foregroundView) + self.layer.addSublayer(self.foregroundCircleLayer) + self.layer.addSublayer(self.growingForegroundCircleLayer) + + self.foregroundView.mask = self.maskView + self.foregroundView.layer.addSublayer(self.foregroundGradientLayer) + + self.maskView.layer.addSublayer(self.maskGradientLayer) + self.maskView.layer.addSublayer(self.maskProgressLayer) + self.maskView.addSubview(self.maskBlobView) + self.maskView.layer.addSublayer(self.maskCircleLayer) + + self.maskBlobView.scaleUpdated = { [weak self] scale in + if let strongSelf = self { + strongSelf.updateGlowScale(strongSelf.isActive ? scale : nil) + } + } + } + + private func setupGradientAnimations() { + if let _ = self.foregroundGradientLayer.animation(forKey: "movement") { + } else { + let previousValue = self.foregroundGradientLayer.startPoint + let newValue: CGPoint + if self.maskBlobView.presentationAudioLevel > 0.22 { + newValue = CGPoint(x: CGFloat.random(in: 0.9 ..< 1.0), y: CGFloat.random(in: 0.15 ..< 0.35)) + } else if self.maskBlobView.presentationAudioLevel > 0.01 { + newValue = CGPoint(x: CGFloat.random(in: 0.57 ..< 0.85), y: CGFloat.random(in: 0.15 ..< 0.45)) + } else { + newValue = CGPoint(x: CGFloat.random(in: 0.6 ..< 0.75), y: CGFloat.random(in: 0.25 ..< 0.45)) + } + self.foregroundGradientLayer.startPoint = newValue + + CATransaction.begin() + + let animation = CABasicAnimation(keyPath: "startPoint") + animation.duration = Double.random(in: 0.8 ..< 1.4) + animation.fromValue = previousValue + animation.toValue = newValue + + CATransaction.setCompletionBlock { [weak self] in + if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy { + self?.setupGradientAnimations() + } + } + + self.foregroundGradientLayer.add(animation, forKey: "movement") + CATransaction.commit() + } + } + + private func setupProgressAnimations() { + if let _ = self.maskProgressLayer.animation(forKey: "progressRotation") { + } else { + self.maskProgressLayer.isHidden = false + + let animation = CABasicAnimation(keyPath: "transform.rotation.z") + animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + animation.duration = 1.0 + animation.fromValue = NSNumber(value: Float(0.0)) + animation.toValue = NSNumber(value: Float.pi * 2.0) + animation.repeatCount = Float.infinity + animation.beginTime = 0.0 + self.maskProgressLayer.add(animation, forKey: "progressRotation") + + let shrinkAnimation = CABasicAnimation(keyPath: "strokeEnd") + shrinkAnimation.fromValue = 1.0 + shrinkAnimation.toValue = 0.0 + shrinkAnimation.duration = 1.0 + shrinkAnimation.beginTime = 0.0 + + let growthAnimation = CABasicAnimation(keyPath: "strokeEnd") + growthAnimation.fromValue = 0.0 + growthAnimation.toValue = 1.0 + growthAnimation.duration = 1.0 + growthAnimation.beginTime = 1.0 + + let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + rotateAnimation.fromValue = 0.0 + rotateAnimation.toValue = CGFloat.pi * 2 + rotateAnimation.isAdditive = true + rotateAnimation.duration = 1.0 + rotateAnimation.beginTime = 1.0 + + let groupAnimation = CAAnimationGroup() + groupAnimation.repeatCount = Float.infinity + groupAnimation.animations = [shrinkAnimation, growthAnimation, rotateAnimation] + groupAnimation.duration = 2.0 + + self.maskProgressLayer.add(groupAnimation, forKey: "progressGrowth") + } + } + + var glowHidden: Bool = false { + didSet { + if self.glowHidden != oldValue { + let initialAlpha = CGFloat(self.maskProgressLayer.opacity) + let targetAlpha: CGFloat = self.glowHidden ? 0.0 : 1.0 + self.maskGradientLayer.opacity = Float(targetAlpha) + self.maskGradientLayer.animateAlpha(from: initialAlpha, to: targetAlpha, duration: 0.2) + } + } + } + + var disableGlowAnimations = false + func updateGlowScale(_ scale: CGFloat?) { + if self.disableGlowAnimations { + return + } + if let scale = scale { + self.maskGradientLayer.transform = CATransform3DMakeScale(0.89 + 0.11 * scale, 0.89 + 0.11 * scale, 1.0) + } else { + let initialScale: CGFloat = ((self.maskGradientLayer.value(forKeyPath: "presentationLayer.transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? (((self.maskGradientLayer.value(forKeyPath: "transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? (0.89)) + let targetScale: CGFloat = self.isActive ? 0.89 : 0.85 + if abs(targetScale - initialScale) > 0.03 { + self.maskGradientLayer.transform = CATransform3DMakeScale(targetScale, targetScale, 1.0) + self.maskGradientLayer.animateScale(from: initialScale, to: targetScale, duration: 0.3) + } + } + } + + enum Gradient { + case speaking + case active + case connecting + case muted + } + + func updateGlowAndGradientAnimations(type: Gradient, previousType: Gradient? = nil, animated: Bool = true) { + let effectivePreviousTyoe = previousType ?? .active + + let scale: CGFloat + if case .speaking = effectivePreviousTyoe { + scale = 0.95 + } else { + scale = 0.8 + } + + let initialScale: CGFloat = ((self.maskGradientLayer.value(forKeyPath: "presentationLayer.transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? (((self.maskGradientLayer.value(forKeyPath: "transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? scale) + let initialColors = self.foregroundGradientLayer.colors + + let outerColor: UIColor? + let activeColor: UIColor? + let targetColors: [CGColor] + let targetScale: CGFloat + switch type { + case .speaking: + targetColors = [activeBlue.cgColor, green.cgColor, green.cgColor] + targetScale = 0.89 + outerColor = UIColor(rgb: 0x134b22) + activeColor = green + case .active: + targetColors = [lightBlue.cgColor, blue.cgColor, blue.cgColor] + targetScale = 0.85 + outerColor = UIColor(rgb: 0x002e5d) + activeColor = blue + case .connecting: + targetColors = [lightBlue.cgColor, blue.cgColor, blue.cgColor] + targetScale = 0.3 + outerColor = nil + activeColor = blue + case .muted: + targetColors = [pink.cgColor, purple.cgColor, purple.cgColor] + targetScale = 0.85 + outerColor = UIColor(rgb: 0x24306b) + activeColor = purple + } + self.updatedColors?(outerColor, activeColor) + + self.maskGradientLayer.transform = CATransform3DMakeScale(targetScale, targetScale, 1.0) + if let _ = previousType { + self.maskGradientLayer.animateScale(from: initialScale, to: targetScale, duration: 0.3) + } else if animated { + self.maskGradientLayer.animateSpring(from: initialScale as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", duration: 0.45) + } + + self.foregroundGradientLayer.colors = targetColors + if animated { + self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) + } + } + + private func playMuteAnimation() { + if self.animationsEnabled { + self.maskBlobView.startAnimating() + } + self.maskBlobView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + if strongSelf.state != .connecting { + return + } + strongSelf.maskBlobView.isHidden = true + strongSelf.maskBlobView.stopAnimating() + strongSelf.maskBlobView.layer.removeAllAnimations() + }) + } + + var animatingDisappearance = false + private func playDeactivationAnimation() { + if self.animatingDisappearance { + return + } + self.animatingDisappearance = true + CATransaction.begin() + CATransaction.setDisableActions(true) + self.growingForegroundCircleLayer.isHidden = false + CATransaction.commit() + + self.disableGlowAnimations = true + self.maskGradientLayer.removeAllAnimations() + self.updateGlowAndGradientAnimations(type: .connecting, previousType: nil) + + if self.animationsEnabled { + self.maskBlobView.startAnimating() + } + self.maskBlobView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + if strongSelf.state != .connecting { + return + } + strongSelf.maskBlobView.isHidden = true + strongSelf.maskBlobView.stopAnimating() + strongSelf.maskBlobView.layer.removeAllAnimations() + }) + + CATransaction.begin() + let growthAnimation = CABasicAnimation(keyPath: "transform.scale") + growthAnimation.fromValue = 0.0 + growthAnimation.toValue = 1.0 + growthAnimation.duration = 0.15 + growthAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) + growthAnimation.isRemovedOnCompletion = false + growthAnimation.fillMode = .forwards + + CATransaction.setCompletionBlock { + self.animatingDisappearance = false + self.growingForegroundCircleLayer.isHidden = true + self.disableGlowAnimations = false + if self.state != .connecting { + return + } + CATransaction.begin() + CATransaction.setDisableActions(true) + self.maskGradientLayer.isHidden = true + self.maskCircleLayer.isHidden = true + self.growingForegroundCircleLayer.removeAllAnimations() + CATransaction.commit() + } + + self.growingForegroundCircleLayer.add(growthAnimation, forKey: "insideGrowth") + CATransaction.commit() + } + + private func playActivationAnimation(active: Bool) { + CATransaction.begin() + CATransaction.setDisableActions(true) + self.maskCircleLayer.isHidden = false + self.maskProgressLayer.isHidden = true + self.maskGradientLayer.isHidden = false + CATransaction.commit() + + self.maskGradientLayer.removeAllAnimations() + self.updateGlowAndGradientAnimations(type: active ? .speaking : .active, previousType: nil) + + self.maskBlobView.isHidden = false + if self.animationsEnabled { + self.maskBlobView.startAnimating() + } + self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) + } + + private func playConnectionAnimation(type: Gradient, completion: @escaping () -> Void) { + CATransaction.begin() + let initialRotation: CGFloat = CGFloat((self.maskProgressLayer.value(forKeyPath: "presentationLayer.transform.rotation.z") as? NSNumber)?.floatValue ?? 0.0) + let initialStrokeEnd: CGFloat = CGFloat((self.maskProgressLayer.value(forKeyPath: "presentationLayer.strokeEnd") as? NSNumber)?.floatValue ?? 1.0) + + self.maskProgressLayer.removeAnimation(forKey: "progressGrowth") + self.maskProgressLayer.removeAnimation(forKey: "progressRotation") + + let duration: Double = (1.0 - Double(initialStrokeEnd)) * 0.3 + + let growthAnimation = CABasicAnimation(keyPath: "strokeEnd") + growthAnimation.fromValue = initialStrokeEnd + growthAnimation.toValue = 1.0 + growthAnimation.duration = duration + growthAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn) + + let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + rotateAnimation.fromValue = initialRotation + rotateAnimation.toValue = initialRotation + CGFloat.pi * 2 + rotateAnimation.isAdditive = true + rotateAnimation.duration = duration + rotateAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn) + + let groupAnimation = CAAnimationGroup() + groupAnimation.animations = [growthAnimation, rotateAnimation] + groupAnimation.duration = duration + + CATransaction.setCompletionBlock { + var active = true + if case .connecting = self.state { + active = false + } + if active { + CATransaction.begin() + CATransaction.setDisableActions(true) + self.foregroundCircleLayer.isHidden = false + self.foregroundCircleLayer.transform = CATransform3DMakeScale(1.0, 1.0, 1.0) + self.maskCircleLayer.isHidden = false + self.maskProgressLayer.isHidden = true + self.maskGradientLayer.isHidden = false + CATransaction.commit() + + completion() + + self.updateGlowAndGradientAnimations(type: type, previousType: nil) + + if case .connecting = self.state { + } else { + self.maskBlobView.isHidden = false + if self.animationsEnabled { + self.maskBlobView.startAnimating() + } + self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) + } + + self.updatedActive?(true) + + CATransaction.begin() + let shrinkAnimation = CABasicAnimation(keyPath: "transform.scale") + shrinkAnimation.fromValue = 1.0 + shrinkAnimation.toValue = 0.00001 + shrinkAnimation.duration = 0.15 + shrinkAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn) + shrinkAnimation.isRemovedOnCompletion = false + shrinkAnimation.fillMode = .forwards + + CATransaction.setCompletionBlock { + CATransaction.begin() + CATransaction.setDisableActions(true) + self.foregroundCircleLayer.isHidden = true + self.foregroundCircleLayer.transform = CATransform3DMakeScale(0.0, 0.0, 1.0) + self.foregroundCircleLayer.removeAllAnimations() + CATransaction.commit() + } + + self.foregroundCircleLayer.add(shrinkAnimation, forKey: "insideShrink") + CATransaction.commit() + } + } + + self.maskProgressLayer.add(groupAnimation, forKey: "progressCompletion") + CATransaction.commit() + } + + private var maskIsCircle = true + private func setupButtonAnimation() { + CATransaction.begin() + CATransaction.setDisableActions(true) + self.backgroundCircleLayer.isHidden = true + self.foregroundCircleLayer.isHidden = true + self.maskCircleLayer.isHidden = false + self.maskProgressLayer.isHidden = true + self.maskGradientLayer.isHidden = true + + let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: floor((self.bounds.height - VoiceChatActionButton.buttonHeight) / 2.0), width: self.bounds.width, height: VoiceChatActionButton.buttonHeight), cornerRadius: 10.0).cgPath + self.maskCircleLayer.path = path + self.maskIsCircle = false + + CATransaction.commit() + + self.updateGlowAndGradientAnimations(type: .muted, previousType: nil) + + self.updatedActive?(true) + } + + private func playScheduledAnimation() { + CATransaction.begin() + CATransaction.setDisableActions(true) + self.maskGradientLayer.isHidden = false + CATransaction.commit() + + let circleFrame = CGRect(origin: CGPoint(x: (self.bounds.width - buttonSize.width) / 2.0, y: (self.bounds.height - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0) + let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath + + let previousPath = self.maskCircleLayer.path + self.maskCircleLayer.path = largerCirclePath + self.maskIsCircle = true + + self.maskCircleLayer.animateSpring(from: previousPath as AnyObject, to: largerCirclePath as AnyObject, keyPath: "path", duration: 0.6, initialVelocity: 0.0, damping: 100.0) + + self.maskBlobView.isHidden = false + if self.animationsEnabled { + self.maskBlobView.startAnimating() + } + self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, damping: 100.0) + + self.disableGlowAnimations = true + self.maskGradientLayer.removeAllAnimations() + self.maskGradientLayer.animateSpring(from: 0.3 as NSNumber, to: 0.85 as NSNumber, keyPath: "transform.scale", duration: 0.45, completion: { [weak self] _ in + self?.disableGlowAnimations = false + }) + } + + var animationsEnabled: Bool = true { + didSet { + self.updateAnimations() + } + } + + var isActive = false + func updateAnimations() { + if !self.isCurrentlyInHierarchy { + self.foregroundGradientLayer.removeAllAnimations() + self.growingForegroundCircleLayer.removeAllAnimations() + self.maskGradientLayer.removeAllAnimations() + self.maskProgressLayer.removeAllAnimations() + self.maskBlobView.stopAnimating() + return + } + + if !self.animationsEnabled { + self.foregroundGradientLayer.removeAllAnimations() + self.maskBlobView.stopAnimating() + } else { + self.setupGradientAnimations() + } + + switch self.state { + case .connecting: + self.updatedActive?(false) + if let transition = self.transition { + self.updateGlowScale(nil) + if case .blob = transition { + self.playDeactivationAnimation() + } else if case .disabled = transition { + self.playDeactivationAnimation() + } + self.transition = nil + } + self.setupProgressAnimations() + self.isActive = false + case let .blob(newActive): + if let transition = self.transition { + let type: Gradient = newActive ? .speaking : .active + if transition == .connecting { + self.playConnectionAnimation(type: type) { [weak self] in + self?.isActive = newActive + } + } else if transition == .disabled { + self.playActivationAnimation(active: newActive) + self.transition = nil + self.isActive = newActive + self.updatedActive?(true) + } else if case let .blob(previousActive) = transition { + self.updateGlowAndGradientAnimations(type: type, previousType: previousActive ? .speaking : .active) + self.transition = nil + self.isActive = newActive + } + self.transition = nil + } else { + if self.animationsEnabled { + self.maskBlobView.startAnimating() + } + } + case .disabled: + self.updatedActive?(true) + self.isActive = false + + if let transition = self.transition { + if case .button = transition { + self.playScheduledAnimation() + } else if case .connecting = transition { + self.playConnectionAnimation(type: .muted) { [weak self] in + self?.isActive = false + } + } else if case let .blob(previousActive) = transition { + self.updateGlowAndGradientAnimations(type: .muted, previousType: previousActive ? .speaking : .active) + self.playMuteAnimation() + } + self.transition = nil + } else { + if self.maskBlobView.isHidden { + self.updateGlowAndGradientAnimations(type: .muted, previousType: nil, animated: false) + self.maskCircleLayer.isHidden = false + self.maskProgressLayer.isHidden = true + self.maskGradientLayer.isHidden = false + self.maskBlobView.isHidden = false + if self.animationsEnabled { + self.maskBlobView.startAnimating() + } + self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) + } + } + case .button: + self.updatedActive?(true) + self.isActive = false + self.setupButtonAnimation() + } + } + + var isDark: Bool = false { + didSet { + if self.isDark != oldValue { + self.updateColors() + } + } + } + + var isSnap: Bool = false { + didSet { + if self.isSnap != oldValue { + self.updateColors() + } + } + } + + var connectingColor: UIColor = UIColor(rgb: 0xb6b6bb) { + didSet { + if self.connectingColor.rgb != oldValue.rgb { + self.updateColors() + } + } + } + + func updateColors() { + let previousColor: CGColor = self.backgroundCircleLayer.fillColor ?? greyColor.cgColor + let targetColor: CGColor + if self.isSnap { + targetColor = self.connectingColor.cgColor + } else if self.isDark { + targetColor = secondaryGreyColor.cgColor + } else { + targetColor = greyColor.cgColor + } + self.backgroundCircleLayer.fillColor = targetColor + self.foregroundCircleLayer.fillColor = targetColor + self.growingForegroundCircleLayer.fillColor = targetColor + self.backgroundCircleLayer.animate(from: previousColor, to: targetColor, keyPath: "fillColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) + self.foregroundCircleLayer.animate(from: previousColor, to: targetColor, keyPath: "fillColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) + self.growingForegroundCircleLayer.animate(from: previousColor, to: targetColor, keyPath: "fillColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) + } + + func update(state: State, animated: Bool) { + var animated = animated + var hadState = true + if !self.hasState { + hadState = false + self.hasState = true + animated = false + } + + if state != self.state || !hadState { + if animated { + self.transition = self.state + } + self.state = state + } + + self.updateAnimations() + } + + var previousSize: CGSize? + override func layout() { + super.layout() + + let sizeUpdated = self.previousSize != self.bounds.size + self.previousSize = self.bounds.size + + let bounds = CGRect(x: (self.bounds.width - areaSize.width) / 2.0, y: (self.bounds.height - areaSize.height) / 2.0, width: areaSize.width, height: areaSize.height) + let center = bounds.center + + self.maskBlobView.frame = CGRect(origin: CGPoint(x: bounds.minX + (bounds.width - blobSize.width) / 2.0, y: bounds.minY + (bounds.height - blobSize.height) / 2.0), size: blobSize) + + let circleFrame = CGRect(origin: CGPoint(x: bounds.minX + (bounds.width - buttonSize.width) / 2.0, y: bounds.minY + (bounds.height - buttonSize.height) / 2.0), size: buttonSize) + self.backgroundCircleLayer.frame = circleFrame + self.foregroundCircleLayer.position = center + self.foregroundCircleLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: circleFrame.width - progressLineWidth, height: circleFrame.height - progressLineWidth)) + self.growingForegroundCircleLayer.position = center + self.growingForegroundCircleLayer.bounds = self.foregroundCircleLayer.bounds + self.maskCircleLayer.frame = self.bounds + + if sizeUpdated && self.maskIsCircle { + CATransaction.begin() + CATransaction.setDisableActions(true) + let circleFrame = CGRect(origin: CGPoint(x: (self.bounds.width - buttonSize.width) / 2.0, y: (self.bounds.height - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0) + let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath + + self.maskCircleLayer.path = largerCirclePath + CATransaction.commit() + } + + self.maskProgressLayer.frame = circleFrame.insetBy(dx: -3.0, dy: -3.0) + self.foregroundView.frame = self.bounds + self.foregroundGradientLayer.frame = self.bounds + self.maskGradientLayer.position = center + self.maskGradientLayer.bounds = bounds + self.maskView.frame = self.bounds + } +} diff --git a/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceChatActionButtonIconNode.swift b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceChatActionButtonIconNode.swift new file mode 100644 index 00000000000..80cf3410df2 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceChatActionButtonIconNode.swift @@ -0,0 +1,143 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ManagedAnimationNode + +public enum VoiceChatActionButtonIconAnimationState: Equatable { + case empty + case start + case subscribe + case unsubscribe + case unmute + case mute + case hand +} + +public final class VoiceChatActionButtonIconNode: ManagedAnimationNode { + private let isColored: Bool + private var iconState: VoiceChatActionButtonIconAnimationState = .mute + + public init(isColored: Bool) { + self.isColored = isColored + super.init(size: CGSize(width: 100.0, height: 100.0)) + + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.1)) + } + + public func enqueueState(_ state: VoiceChatActionButtonIconAnimationState) { + guard self.iconState != state else { + return + } + + let previousState = self.iconState + self.iconState = state + + if state != .empty { + self.alpha = 1.0 + } + switch previousState { + case .empty: + switch state { + case .start: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001)) + default: + break + } + case .subscribe: + switch state { + case .unsubscribe: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceCancelReminder"))) + case .mute: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceSetReminderToMute"))) + case .hand: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceSetReminderToRaiseHand"))) + default: + break + } + case .unsubscribe: + switch state { + case .subscribe: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceSetReminder"))) + case .mute: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceCancelReminderToMute"))) + case .hand: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceCancelReminderToRaiseHand"))) + default: + break + } + case .start: + switch state { + case .mute: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart"))) + default: + break + } + case .unmute: + switch state { + case .mute: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceMute"))) + case .hand: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmuteToRaiseHand"))) + default: + break + } + case .mute: + switch state { + case .start: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001)) + case .unmute: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute"))) + case .hand: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceMuteToRaiseHand"))) + case .subscribe: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceSetReminderToRaiseHand"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001)) + case .unsubscribe: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceCancelReminderToRaiseHand"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001)) + case .empty: + self.alpha = 0.0 + default: + break + } + case .hand: + switch state { + case .mute, .unmute: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceRaiseHandToMute"))) + default: + break + } + } + } + + public func playRandomAnimation() { + if case .hand = self.iconState { + if let next = self.trackStack.first, case let .local(name) = next.source, name.hasPrefix("VoiceHand_") { + return + } + + var useTiredAnimation = false + var useAngryAnimation = false + let val = Float.random(in: 0.0..<1.0) + if val <= 0.01 { + useTiredAnimation = true + } else if val <= 0.05 { + useAngryAnimation = true + } + + let normalAnimations = ["VoiceHand_1", "VoiceHand_2", "VoiceHand_3", "VoiceHand_4", "VoiceHand_7", "VoiceHand_8"] + let tiredAnimations = ["VoiceHand_5", "VoiceHand_6"] + let angryAnimations = ["VoiceHand_9", "VoiceHand_10"] + let animations: [String] + if useTiredAnimation { + animations = tiredAnimations + } else if useAngryAnimation { + animations = angryAnimations + } else { + animations = normalAnimations + } + if let animationName = animations.randomElement() { + self.trackTo(item: ManagedAnimationItem(source: .local(animationName))) + } + } + } +} diff --git a/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceChatRaiseHandNode.swift b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceChatRaiseHandNode.swift new file mode 100644 index 00000000000..d208d20faae --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceChatRaiseHandNode.swift @@ -0,0 +1,53 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import AnimationUI +import AppBundle + +public final class VoiceChatRaiseHandNode: ASDisplayNode { + private let animationNode: AnimationNode + private let color: UIColor? + private var playedOnce = false + + public init(color: UIColor?) { + self.color = color + if let color = color, let url = getAppBundle().url(forResource: "anim_hand1", withExtension: "json"), let data = try? Data(contentsOf: url) { + self.animationNode = AnimationNode(animationData: transformedWithColors(data: data, colors: [(UIColor(rgb: 0xffffff), color)])) + } else { + self.animationNode = AnimationNode(animation: "anim_hand1", colors: nil, scale: 0.5) + } + super.init() + self.addSubnode(self.animationNode) + } + + public func playRandomAnimation() { + guard self.playedOnce else { + self.playedOnce = true + self.animationNode.play() + return + } + + guard !self.animationNode.isPlaying else { + self.animationNode.completion = { [weak self] in + self?.playRandomAnimation() + } + return + } + + self.animationNode.completion = nil + if let animationName = ["anim_hand1", "anim_hand2", "anim_hand3", "anim_hand4"].randomElement() { + if let color = color, let url = getAppBundle().url(forResource: animationName, withExtension: "json"), let data = try? Data(contentsOf: url) { + self.animationNode.setAnimation(data: transformedWithColors(data: data, colors: [(UIColor(rgb: 0xffffff), color)])) + } else { + self.animationNode.setAnimation(name: animationName) + } + self.animationNode.play() + } + } + + override public func layout() { + super.layout() + self.animationNode.frame = self.bounds + } +} diff --git a/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/VoiceChatActionButtonAssets.xcassets/Contents.json b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/VoiceChatActionButtonAssets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/VoiceChatActionButtonAssets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/VoiceChatActionButtonAssets.xcassets/VoiceChatActionButton/Contents.json b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/VoiceChatActionButtonAssets.xcassets/VoiceChatActionButton/Contents.json new file mode 100644 index 00000000000..6e965652df6 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/VoiceChatActionButtonAssets.xcassets/VoiceChatActionButton/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Components/CameraButtonComponent/Sources/CameraButtonComponent.swift b/submodules/TelegramUI/Components/CameraButtonComponent/Sources/CameraButtonComponent.swift index 326dd928820..c7345f74f1d 100644 --- a/submodules/TelegramUI/Components/CameraButtonComponent/Sources/CameraButtonComponent.swift +++ b/submodules/TelegramUI/Components/CameraButtonComponent/Sources/CameraButtonComponent.swift @@ -73,7 +73,7 @@ public final class CameraButton: Component { } } - private func updateScale(transition: Transition) { + private func updateScale(transition: ComponentTransition) { guard let component = self.component else { return } @@ -147,7 +147,7 @@ public final class CameraButton: Component { super.cancelTracking(with: event) } - func update(component: CameraButton, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: CameraButton, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if let currentId = self.component?.content.id, currentId != component.content.id { let previousContentView = self.contentView @@ -200,7 +200,7 @@ public final class CameraButton: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 3051825902e..55084110b92 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -2235,7 +2235,7 @@ public class CameraScreen: ViewController { self.cameraIsActive = false self.requestUpdateLayout(hasAppeared: self.hasAppeared, transition: .immediate) - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) { view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) transition.setAlpha(view: view, alpha: 0.0) @@ -2313,7 +2313,7 @@ public class CameraScreen: ViewController { self.cameraIsActive = true self.requestUpdateLayout(hasAppeared: self.hasAppeared, transition: .immediate) - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) { view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) transition.setAlpha(view: view, alpha: 1.0) @@ -2466,14 +2466,14 @@ public class CameraScreen: ViewController { return result } - func requestUpdateLayout(hasAppeared: Bool, transition: Transition) { + func requestUpdateLayout(hasAppeared: Bool, transition: ComponentTransition) { if let layout = self.validLayout { self.containerLayoutUpdated(layout: layout, forceUpdate: true, hasAppeared: hasAppeared, transition: transition) } } fileprivate var hasAppeared = false - func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, hasAppeared: Bool = false, transition: Transition) { + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, hasAppeared: Bool = false, transition: ComponentTransition) { guard let controller = self.controller else { return } @@ -2808,7 +2808,7 @@ public class CameraScreen: ViewController { public var isEmbedded = false - fileprivate func updateCameraState(_ f: (CameraState) -> CameraState, transition: Transition) { + fileprivate func updateCameraState(_ f: (CameraState) -> CameraState, transition: ComponentTransition) { self.node.cameraState = f(self.node.cameraState) self.node.requestUpdateLayout(hasAppeared: self.node.hasAppeared, transition: transition) } @@ -3211,7 +3211,7 @@ public class CameraScreen: ViewController { super.containerLayoutUpdated(layout, transition: transition) if !self.isDismissed { - (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) } } } @@ -3284,7 +3284,7 @@ private final class DualIconComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: DualIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: DualIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -3302,7 +3302,7 @@ private final class DualIconComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift index fcd1b04b8d9..eb5b47c7280 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift @@ -35,8 +35,8 @@ private final class ShutterButtonContentComponent: Component { let shutterState: ShutterButtonState let blobState: ShutterBlobView.BlobState let highlightedAction: ActionSlot - let updateOffsetX: ActionSlot<(CGFloat, Transition)> - let updateOffsetY: ActionSlot<(CGFloat, Transition)> + let updateOffsetX: ActionSlot<(CGFloat, ComponentTransition)> + let updateOffsetY: ActionSlot<(CGFloat, ComponentTransition)> init( isTablet: Bool, @@ -45,8 +45,8 @@ private final class ShutterButtonContentComponent: Component { shutterState: ShutterButtonState, blobState: ShutterBlobView.BlobState, highlightedAction: ActionSlot, - updateOffsetX: ActionSlot<(CGFloat, Transition)>, - updateOffsetY: ActionSlot<(CGFloat, Transition)> + updateOffsetX: ActionSlot<(CGFloat, ComponentTransition)>, + updateOffsetY: ActionSlot<(CGFloat, ComponentTransition)> ) { self.isTablet = isTablet self.hasAppeared = hasAppeared @@ -106,11 +106,11 @@ private final class ShutterButtonContentComponent: Component { return } let scale: CGFloat = isHighlighted ? 0.8 : 1.0 - let transition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)) transition.setTransform(view: blobView, transform: CATransform3DMakeScale(scale, scale, 1.0)) } - func update(component: ShutterButtonContentComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: ShutterButtonContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component if component.hasAppeared && self.blobView == nil { @@ -245,7 +245,7 @@ private final class ShutterButtonContentComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -325,7 +325,7 @@ final class FlipButtonContentComponent: Component { self.darkIcon.add(darkAnimation, forKey: "transform.rotation.z") } - func update(component: FlipButtonContentComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: FlipButtonContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component component.action.connect { [weak self] _ in @@ -353,7 +353,7 @@ final class FlipButtonContentComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -404,7 +404,7 @@ final class LockContentComponent: Component { preconditionFailure() } - func update(component: LockContentComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: LockContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component let size = CGSize(width: 30.0, height: 30.0) @@ -428,7 +428,7 @@ final class LockContentComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -599,8 +599,8 @@ final class CaptureControlsComponent: Component { private let leftGuide = SimpleLayer() private let rightGuide = SimpleLayer() - private let shutterUpdateOffsetX = ActionSlot<(CGFloat, Transition)>() - private let shutterUpdateOffsetY = ActionSlot<(CGFloat, Transition)>() + private let shutterUpdateOffsetX = ActionSlot<(CGFloat, ComponentTransition)>() + private let shutterUpdateOffsetY = ActionSlot<(CGFloat, ComponentTransition)>() private let shutterHightlightedAction = ActionSlot() @@ -697,13 +697,13 @@ final class CaptureControlsComponent: Component { private var shutterOffsetX: CGFloat = 0.0 private var shutterOffsetY: CGFloat = 0.0 - private func updateShutterOffsetX(_ offsetX: CGFloat, transition: Transition) { + private func updateShutterOffsetX(_ offsetX: CGFloat, transition: ComponentTransition) { self.shutterOffsetX = offsetX self.shutterUpdateOffsetX.invoke((offsetX, transition)) self.state?.updated(transition: transition) } - private func updateShutterOffsetY(_ offsetY: CGFloat, transition: Transition) { + private func updateShutterOffsetY(_ offsetY: CGFloat, transition: ComponentTransition) { self.shutterOffsetY = offsetY self.shutterUpdateOffsetY.invoke((offsetY, transition)) self.state?.updated(transition: transition) @@ -720,8 +720,8 @@ final class CaptureControlsComponent: Component { return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range } - var scheduledXOffsetUpdate: (CGFloat, Transition)? - var scheduledYOffsetUpdate: (CGFloat, Transition)? + var scheduledXOffsetUpdate: (CGFloat, ComponentTransition)? + var scheduledYOffsetUpdate: (CGFloat, ComponentTransition)? let previousPanBlobState = self.panBlobState let location = gestureRecognizer.location(in: self) @@ -766,7 +766,7 @@ final class CaptureControlsComponent: Component { self.panBlobState = .video isBanding = true } - var transition: Transition = .immediate + var transition: ComponentTransition = .immediate if let wasBanding = self.wasBanding, wasBanding != isBanding { //self.hapticFeedback.impact(.light) transition = .spring(duration: 0.35) @@ -824,7 +824,7 @@ final class CaptureControlsComponent: Component { self.panBlobState = .video isBanding = true } - var transition: Transition = .immediate + var transition: ComponentTransition = .immediate if let wasBanding = self.wasBanding, wasBanding != isBanding { //self.hapticFeedback.impact(.light) transition = .spring(duration: 0.35) @@ -857,7 +857,7 @@ final class CaptureControlsComponent: Component { } private var animatedOut = false - func animateOutToEditor(transition: Transition) { + func animateOutToEditor(transition: ComponentTransition) { self.animatedOut = true if let view = self.galleryButtonView.view { @@ -876,7 +876,7 @@ final class CaptureControlsComponent: Component { } } - func animateInFromEditor(transition: Transition) { + func animateInFromEditor(transition: ComponentTransition) { self.animatedOut = false if let view = self.galleryButtonView.view { @@ -895,7 +895,7 @@ final class CaptureControlsComponent: Component { } } - func update(component: CaptureControlsComponent, state: State, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: CaptureControlsComponent, state: State, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let previousShutterState = self.component?.shutterState ?? .generic self.component = component self.state = state @@ -1227,7 +1227,7 @@ final class CaptureControlsComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, state: state, availableSize: availableSize, transition: transition) } } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift index 55287c4c530..78c06465d2f 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift @@ -80,7 +80,7 @@ private final class FlashColorComponent: Component { super.cancelTracking(with: event) } - func update(component: FlashColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: FlashColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let contentSize = CGSize(width: 30.0, height: 30.0) self.contentView.frame = CGRect(origin: .zero, size: contentSize) @@ -131,7 +131,7 @@ private final class FlashColorComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -208,7 +208,7 @@ final class FlashTintControlComponent: Component { self.component?.dismiss() } - func update(component: FlashTintControlComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: FlashTintControlComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let isFirstTime = self.component == nil self.component = component @@ -319,7 +319,7 @@ final class FlashTintControlComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -374,7 +374,7 @@ private final class SliderView: UIView { fatalError("init(coder:) has not been implemented") } - private func updateValue(transition: Transition = .immediate) { + private func updateValue(transition: ComponentTransition = .immediate) { let width = self.frame.width let range = self.maxValue - self.minValue diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift index 001b3a22217..cd4a2a61048 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift @@ -128,21 +128,21 @@ final class ModeComponent: Component { } private var animatedOut = false - func animateOutToEditor(transition: Transition) { + func animateOutToEditor(transition: ComponentTransition) { self.animatedOut = true transition.setAlpha(view: self.containerView, alpha: 0.0) transition.setSublayerTransform(view: self.containerView, transform: CATransform3DMakeTranslation(0.0, -buttonSize.height, 0.0)) } - func animateInFromEditor(transition: Transition) { + func animateInFromEditor(transition: ComponentTransition) { self.animatedOut = false transition.setAlpha(view: self.containerView, alpha: 1.0) transition.setSublayerTransform(view: self.containerView, transform: CATransform3DIdentity) } - func update(component: ModeComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: ModeComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component let isTablet = component.isTablet @@ -207,7 +207,7 @@ final class ModeComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -246,7 +246,7 @@ final class HintLabelComponent: Component { preconditionFailure() } - func update(component: HintLabelComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: HintLabelComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component @@ -293,7 +293,7 @@ final class HintLabelComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/PlaceholderComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/PlaceholderComponent.swift index bb9055cd3f3..1fb49b88dcc 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/PlaceholderComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/PlaceholderComponent.swift @@ -61,7 +61,7 @@ final class PlaceholderComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: PlaceholderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PlaceholderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -216,7 +216,7 @@ final class PlaceholderComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift index a72251da55b..094fe890b58 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift @@ -8,11 +8,11 @@ import MetalImageView private final class PropertyAnimation { let from: T let to: T - let animation: Transition.Animation + let animation: ComponentTransition.Animation let startTimestamp: Double private let interpolator: (Interpolatable, Interpolatable, CGFloat) -> Interpolatable - init(fromValue: T, toValue: T, animation: Transition.Animation, startTimestamp: Double) { + init(fromValue: T, toValue: T, animation: ComponentTransition.Animation, startTimestamp: Double) { self.from = fromValue self.to = toValue self.animation = animation @@ -41,7 +41,7 @@ private final class AnimatableProperty { self.presentationValue = value } - func update(value: T, transition: Transition = .immediate) { + func update(value: T, transition: ComponentTransition = .immediate) { let currentTimestamp = CACurrentMediaTime() if case .none = transition.animation { if let animation = self.animation, case let .curve(duration, curve) = animation.animation { @@ -295,7 +295,7 @@ final class ShutterBlobView: UIView { self.displayLink?.invalidate() } - func updateState(_ state: BlobState, tintColor: UIColor, transition: Transition = .immediate) { + func updateState(_ state: BlobState, tintColor: UIColor, transition: ComponentTransition = .immediate) { guard self.state != state else { return } @@ -310,7 +310,7 @@ final class ShutterBlobView: UIView { self.tick() } - func updatePrimaryOffsetX(_ offset: CGFloat, transition: Transition = .immediate) { + func updatePrimaryOffsetX(_ offset: CGFloat, transition: ComponentTransition = .immediate) { guard self.frame.height > 0.0 else { return } @@ -320,7 +320,7 @@ final class ShutterBlobView: UIView { self.tick() } - func updatePrimaryOffsetY(_ offset: CGFloat, transition: Transition = .immediate) { + func updatePrimaryOffsetY(_ offset: CGFloat, transition: ComponentTransition = .immediate) { guard self.frame.height > 0.0 else { return } @@ -330,7 +330,7 @@ final class ShutterBlobView: UIView { self.tick() } - func updateSecondaryOffsetX(_ offset: CGFloat, transition: Transition = .immediate) { + func updateSecondaryOffsetX(_ offset: CGFloat, transition: ComponentTransition = .immediate) { guard self.frame.height > 0.0 else { return } @@ -340,7 +340,7 @@ final class ShutterBlobView: UIView { self.tick() } - func updateSecondaryOffsetY(_ offset: CGFloat, transition: Transition = .immediate) { + func updateSecondaryOffsetY(_ offset: CGFloat, transition: ComponentTransition = .immediate) { guard self.frame.height > 0.0 else { return } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ZoomComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ZoomComponent.swift index a1499a7dc76..6c5b759e3bd 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ZoomComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ZoomComponent.swift @@ -113,7 +113,7 @@ final class ZoomComponent: Component { self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) } - func update(component: ZoomComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: ZoomComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component let sideInset: CGFloat = 3.0 @@ -166,7 +166,7 @@ final class ZoomComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift index 66b4bfd8f77..1917457a40d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift @@ -210,7 +210,7 @@ public final class ChatAvatarNavigationNode: ASDisplayNode { } let _ = avatarStoryView.update( - transition: Transition(transition), + transition: ComponentTransition(transition), component: AnyComponent(AvatarStoryIndicatorComponent( hasUnseen: storyData.hasUnseen, hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends, diff --git a/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift index ecfe9f9f54b..8ee63a28acc 100644 --- a/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift @@ -481,15 +481,15 @@ public final class ChatBotInfoItemNode: ListViewItemNode { case .none, .ignore: break case let .url(url): - item.controllerInteraction.longTap(.url(url.url), nil) + item.controllerInteraction.longTap(.url(url.url), ChatControllerInteraction.LongTapParams()) case let .peerMention(peerId, mention, _): - item.controllerInteraction.longTap(.peerMention(peerId, mention), nil) + item.controllerInteraction.longTap(.peerMention(peerId, mention), ChatControllerInteraction.LongTapParams()) case let .textMention(name): - item.controllerInteraction.longTap(.mention(name), nil) + item.controllerInteraction.longTap(.mention(name), ChatControllerInteraction.LongTapParams()) case let .botCommand(command): - item.controllerInteraction.longTap(.command(command), nil) + item.controllerInteraction.longTap(.command(command), ChatControllerInteraction.LongTapParams()) case let .hashtag(_, hashtag): - item.controllerInteraction.longTap(.hashtag(hashtag), nil) + item.controllerInteraction.longTap(.hashtag(hashtag), ChatControllerInteraction.LongTapParams()) default: break } diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD index ee803ef3845..70f4619d5bd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD @@ -22,11 +22,11 @@ swift_library( "//submodules/TelegramPresentationData", "//submodules/AlertUI", "//submodules/PresentationDataUtils", - "//submodules/PeerInfoUI", "//submodules/UndoUI", "//submodules/ChatPresentationInterfaceState", "//submodules/TelegramUI/Components/Chat/ChatInputPanelNode", "//submodules/AccountContext", + "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift index a3e9810c3b4..115fd605314 100644 --- a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift @@ -11,11 +11,11 @@ import SwiftSignalKit import TelegramPresentationData import AlertUI import PresentationDataUtils -import PeerInfoUI import UndoUI import ChatPresentationInterfaceState import ChatInputPanelNode import AccountContext +import OldChannelsController private enum SubscriberAction: Equatable { case join diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift index ac62e628a69..d4388e4be23 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift @@ -904,12 +904,12 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC linkTextButton.layer.removeAnimation(forKey: "transform.scale") if animateScale { - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) transition.setScale(layer: linkTextButton.layer, scale: topScale) } } else { if animateScale { - let transition = Transition(animation: .none) + let transition = ComponentTransition(animation: .none) transition.setScale(layer: linkTextButton.layer, scale: 1.0) linkTextButton.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak linkTextButton] _ in @@ -1469,12 +1469,12 @@ private final class EmptyAttachedDescriptionNode: HighlightTrackingButtonNode { self.layer.removeAnimation(forKey: "transform.scale") if animateScale { - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) transition.setScale(layer: self.layer, scale: topScale) } } else { if animateScale { - let transition = Transition(animation: .none) + let transition = ComponentTransition(animation: .none) transition.setScale(layer: self.layer, scale: 1.0) self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in diff --git a/submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode/Sources/ChatHistorySearchContainerNode.swift b/submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode/Sources/ChatHistorySearchContainerNode.swift index 32331874e92..e45a3a023c2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode/Sources/ChatHistorySearchContainerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode/Sources/ChatHistorySearchContainerNode.swift @@ -27,7 +27,7 @@ private extension ListMessageItemInteraction { }, openInstantPage: { message, data in controllerInteraction.openInstantPage(message, data) }, longTap: { action, message in - controllerInteraction.longTap(action, message) + controllerInteraction.longTap(action, ChatControllerInteraction.LongTapParams(message: message)) }, getHiddenMedia: { return controllerInteraction.hiddenMedia }) diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index 13552cf68f0..802c7ee1557 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -303,7 +303,7 @@ public final class ChatInlineSearchResultsListComponent: Component { } } - func update(component: ChatInlineSearchResultsListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChatInlineSearchResultsListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -871,7 +871,7 @@ public final class ChatInlineSearchResultsListComponent: Component { } } - let fadeTransition = Transition.easeInOut(duration: 0.25) + let fadeTransition = ComponentTransition.easeInOut(duration: 0.25) if component.showEmptyResults, let appliedContentsState = self.appliedContentsState, appliedContentsState.entries.isEmpty, case let .search(query, _) = component.contents, !query.isEmpty { let sideInset: CGFloat = 44.0 let emptyAnimationHeight = 148.0 @@ -892,11 +892,19 @@ public final class ChatInlineSearchResultsListComponent: Component { environment: {}, containerSize: availableSize ) + + let placeholderText: String + if query.hasPrefix("$") { + placeholderText = component.presentation.strings.HashtagSearch_NoResultsQueryCashtagDescription(query).string + } else { + placeholderText = component.presentation.strings.HashtagSearch_NoResultsQueryDescription(query).string + } + let emptyResultsTextSize = self.emptyResultsText.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: component.presentation.strings.HashtagSearch_NoResultsQueryDescription(query).string, font: Font.regular(15.0), textColor: component.presentation.theme.list.itemSecondaryTextColor)), + text: .plain(NSAttributedString(string: placeholderText, font: Font.regular(15.0), textColor: component.presentation.theme.list.itemSecondaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0 ) @@ -976,7 +984,7 @@ public final class ChatInlineSearchResultsListComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/BUILD index a4033863612..4d86a5de0e2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/TelegramPresentationData", "//submodules/AccountContext", "//submodules/WallpaperBackgroundNode", + "//submodules/UrlHandling", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift index 49cb6a9f55a..730934d938b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift @@ -7,6 +7,7 @@ import Display import TelegramPresentationData import AccountContext import WallpaperBackgroundNode +import UrlHandling private let titleFont = Font.medium(16.0) @@ -178,7 +179,9 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { case .text: iconImage = incoming ? graphics.chatBubbleActionButtonIncomingMessageIconImage : graphics.chatBubbleActionButtonOutgoingMessageIconImage case let .url(value): - if value.lowercased().contains("?startgroup=") { + if isTelegramMeLink(value), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, url: value), case .peer(_, .appStart) = internalUrl { + iconImage = incoming ? graphics.chatBubbleActionButtonIncomingWebAppIconImage : graphics.chatBubbleActionButtonOutgoingWebAppIconImage + } else if value.lowercased().contains("?startgroup=") { iconImage = incoming ? graphics.chatBubbleActionButtonIncomingAddToChatIconImage : graphics.chatBubbleActionButtonOutgoingAddToChatIconImage } else { iconImage = incoming ? graphics.chatBubbleActionButtonIncomingLinkIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage @@ -225,7 +228,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { let titleColor = bubbleVariableColor(variableColor: messageTheme.actionButtonsTextColor, wallpaper: theme.wallpaper) let attributedTitle: NSAttributedString if isStarsPayment { - let updatedTitle = title.replacingOccurrences(of: "⭐️", with: " # ") + let updatedTitle = title.replacingOccurrences(of: "⭐️", with: " # ") let buttonAttributedString = NSMutableAttributedString(string: updatedTitle, font: titleFont, textColor: titleColor, paragraphAlignment: .center) if let range = buttonAttributedString.string.range(of: "#"), let starImage = UIImage(bundleImageName: "Item List/PremiumIcon") { buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 0b3450db149..9a4942392f6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -592,12 +592,14 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - var isPlaying = self.visibilityStatus == true && !self.forceStopAnimations + let isPlaying = self.visibilityStatus == true && !self.forceStopAnimations + + var canPlayEffects = isPlaying if !item.controllerInteraction.canReadHistory { - isPlaying = false + canPlayEffects = false } - if !isPlaying { + if !canPlayEffects { self.removeAdditionalAnimations() self.removeEffectAnimations() } @@ -630,7 +632,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - if isPlaying, let animationNode = self.animationNode as? AnimatedStickerNode { + if canPlayEffects, let animationNode = self.animationNode as? AnimatedStickerNode { var effectAlreadySeen = true if item.message.flags.contains(.Incoming) { if let unreadRange = item.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: item.message.id.peerId, namespace: item.message.id.namespace)] { @@ -1080,7 +1082,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.message, isInline: item.associatedData.isInline), + canViewReactionList: canViewMessageReactionList(message: item.message), animationCache: item.controllerInteraction.presentationContext.animationCache, animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) @@ -2034,7 +2036,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) } - let decorationNode = transitionNode.add(decorationView: additionalAnimationNode.view, itemNode: self) + let decorationNode = transitionNode.add(decorationView: additionalAnimationNode.view, itemNode: self, aboveEverything: true) additionalAnimationNode.completed = { [weak self, weak decorationNode, weak transitionNode] _ in guard let decorationNode = decorationNode else { return diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index a7fff6e9afb..4c4c6237fed 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -213,8 +213,12 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { let badgeFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)) var incoming = message.effectivelyIncoming(context.account.peerId) - if let subject = associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { - incoming = false + if let subject = associatedData.subject, case let .messageOptions(_, _, info) = subject { + if case .forward = info { + incoming = false + } else if case let .link(link) = info, link.isCentered { + incoming = true + } } var isReplyThread = false @@ -394,6 +398,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } else { let contentMode: InteractiveMediaNodeContentMode = contentMediaAspectFilled ? .aspectFill : .aspectFit + let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: contentMediaValue) + let (_, initialImageWidth, refineLayout) = makeContentMedia( context, presentationData, @@ -402,7 +408,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { attributes, contentMediaValue, nil, - .full, + nil, + automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)), @@ -702,6 +709,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { var statusLayoutAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))? if case .customChatContents = associatedData.subject { + } else if !presentationData.chatBubbleCorners.hasTails { + } else if case let .messageOptions(_, _, info) = associatedData.subject, case let .link(link) = info, link.isCentered { } else if case let .linear(_, bottom) = position { switch bottom { case .None, .Neighbour(_, .footer, _): @@ -728,7 +737,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { replyCount: dateReplies, isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: message, isInline: associatedData.isInline), + canViewReactionList: canViewMessageReactionList(message: message), animationCache: controllerInteraction.presentationContext.animationCache, animationRenderer: controllerInteraction.presentationContext.animationRenderer )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift index 68b9fecff98..5109dc625c7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift @@ -118,7 +118,7 @@ public enum ChatMessageBubbleContentPosition { public enum ChatMessageBubblePreparePosition { case linear(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition) - case mosaic(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition) + case mosaic(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition, index: Int?) } public struct ChatMessageBubbleContentTapAction { @@ -163,11 +163,13 @@ public struct ChatMessageBubbleContentTapAction { } public var content: Content + public var rects: [CGRect]? public var hasLongTapAction: Bool public var activate: (() -> Promise?)? - public init(content: Content, hasLongTapAction: Bool = true, activate: (() -> Promise?)? = nil) { + public init(content: Content, rects: [CGRect]? = nil, hasLongTapAction: Bool = true, activate: (() -> Promise?)? = nil) { self.content = content + self.rects = rects self.hasLongTapAction = hasLongTapAction self.activate = activate } @@ -206,6 +208,8 @@ open class ChatMessageBubbleContentNode: ASDisplayNode { return false } + open var index: Int? + public weak var itemNode: ChatMessageItemNodeProtocol? public weak var bubbleBackgroundNode: ChatMessageBackground? public weak var bubbleBackdropNode: ChatMessageBubbleBackdrop? diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD index b39d4b165b6..9225db7cfbd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD @@ -81,6 +81,8 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageUnlockMediaNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageStarsMediaInfoNode", "//submodules/UIKitRuntimeUtils", "//submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode", "//submodules/AnimatedStickerNode", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index d72385755cf..d67e2f811c9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -90,6 +90,8 @@ import ChatMessageGiftBubbleContentNode import ChatMessageGiveawayBubbleContentNode import ChatMessageJoinedChannelBubbleContentNode import ChatMessageFactCheckBubbleContentNode +import ChatMessageUnlockMediaNode +import ChatMessageStarsMediaInfoNode import UIKitRuntimeUtils import ChatMessageTransitionNode import AnimatedStickerNode @@ -97,9 +99,17 @@ import TelegramAnimatedStickerNode import LottieMetal private struct BubbleItemAttributes { + var index: Int? var isAttachment: Bool var neighborType: ChatMessageBubbleRelativePosition.NeighbourType var neighborSpacing: ChatMessageBubbleRelativePosition.NeighbourSpacing + + init(index: Int? = nil, isAttachment: Bool, neighborType: ChatMessageBubbleRelativePosition.NeighbourType, neighborSpacing: ChatMessageBubbleRelativePosition.NeighbourSpacing) { + self.index = index + self.isAttachment = isAttachment + self.neighborType = neighborType + self.neighborSpacing = neighborSpacing + } } private final class ChatMessageBubbleClippingNode: ASDisplayNode { @@ -146,7 +156,13 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ var isFile = false inner: for media in message.media { - if let _ = media as? TelegramMediaImage { + if let media = media as? TelegramMediaPaidContent { + var index = 0 + for _ in media.extendedMedia { + result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(index: index, isAttachment: false, neighborType: .media, neighborSpacing: .default))) + index += 1 + } + } else if let _ = media as? TelegramMediaImage { if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), message.text.isEmpty { messageWithCaptionToAdd = (message, itemAttributes) } @@ -284,8 +300,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ result.insert((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)), at: 0) } else { result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default))) + needReactions = false } - needReactions = false } } else { if case .group = item.content { @@ -645,6 +661,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private var actionButtonsNode: ChatMessageActionButtonsNode? private var reactionButtonsNode: ChatMessageReactionButtonsNode? + private var unlockButtonNode: ChatMessageUnlockMediaNode? + private var mediaInfoNode: ChatMessageStarsMediaInfoNode? + private var shareButtonNode: ChatMessageShareButton? private var trButtonNode: ChatMessageShareButton? @@ -690,6 +709,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI replyInfoNode.visibility = self.visibility != .none } + if let unlockButtonNode = self.unlockButtonNode { + unlockButtonNode.visibility = self.visibility != .none + } + self.visibilityStatus = self.visibility != .none self.updateVisibility() @@ -877,6 +900,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI deinit { } + override public func updateTrailingItemSpace(_ height: CGFloat, transition: ContainedViewLayoutTransition) { + if height.isLessThanOrEqualTo(0.0) { + transition.updateFrame(node: self.mainContainerNode, frame: CGRect(origin: CGPoint(), size: self.mainContainerNode.bounds.size)) + } else { + transition.updateFrame(node: self.mainContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -floorToScreenPixels(height / 2.0)), size: self.mainContainerNode.bounds.size)) + } + } + override public func cancelInsertionAnimations() { self.shadowNode.layer.removeAllAnimations() @@ -1206,6 +1237,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let replyInfoNode = strongSelf.replyInfoNode, replyInfoNode.frame.contains(point) { return .waitForSingleTap } + if let unlockButtonNode = strongSelf.unlockButtonNode, unlockButtonNode.frame.contains(point) { + if let _ = unlockButtonNode.hitTest(strongSelf.view.convert(point, to: unlockButtonNode.view), with: nil) { + return .fail + } + } if let forwardInfoNode = strongSelf.forwardInfoNode, forwardInfoNode.frame.contains(point) { if forwardInfoNode.hasAction(at: strongSelf.view.convert(point, to: forwardInfoNode.view)) { return .fail @@ -1356,10 +1392,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { - var currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))] = [] + var currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, Int?, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))] = [] for contentNode in self.contentNodes { if let message = contentNode.item?.message { - currentContentClassesPropertiesAndLayouts.append((message, type(of: contentNode) as AnyClass, contentNode.supportsMosaic, contentNode.asyncLayoutContent())) + currentContentClassesPropertiesAndLayouts.append((message, type(of: contentNode) as AnyClass, contentNode.supportsMosaic, contentNode.index, contentNode.asyncLayoutContent())) } else { assertionFailure() } @@ -1374,6 +1410,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let replyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode) let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode) let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode) + let unlockButtonLayout = ChatMessageUnlockMediaNode.asyncLayout(self.unlockButtonNode) + let mediaInfoLayout = ChatMessageStarsMediaInfoNode.asyncLayout(self.mediaInfoNode) let mosaicStatusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.mosaicStatusNode) @@ -1399,6 +1437,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI replyInfoLayout: replyInfoLayout, actionButtonsLayout: actionButtonsLayout, reactionButtonsLayout: reactionButtonsLayout, + unlockButtonLayout: unlockButtonLayout, + mediaInfoLayout: mediaInfoLayout, mosaicStatusLayout: mosaicStatusLayout, wantTrButton: self.wantTrButton, layoutConstants: layoutConstants, @@ -1411,7 +1451,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private static func beginLayout( selfReference: Weak, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool, - currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))], + currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, Int?, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))], authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), viaMeasureLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), @@ -1421,6 +1461,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, (CGSize, Bool, ListViewItemUpdateAnimation) -> ChatMessageReplyInfoNode), actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, WallpaperBackgroundNode?, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)), reactionButtonsLayout: (ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)), + unlockButtonLayout: (ChatMessageUnlockMediaNode.Arguments) -> (CGSize, (Bool) -> ChatMessageUnlockMediaNode), + mediaInfoLayout: (ChatMessageStarsMediaInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode), mosaicStatusLayout: (ChatMessageDateAndStatusNode.Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)), wantTrButton: [(Bool, [String])], layoutConstants: ChatMessageItemLayoutConstants, @@ -1775,15 +1817,15 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI maximumContentWidth = max(0.0, maximumContentWidth) var contentPropertiesAndPrepareLayouts: [(Message, Bool, ChatMessageEntryAttributes, BubbleItemAttributes, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))] = [] - var addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode)]? + var addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode, Int?)]? for contentNodeItemValue in contentNodeMessagesAndClasses { let contentNodeItem = contentNodeItemValue as (message: Message, type: AnyClass, attributes: ChatMessageEntryAttributes, bubbleAttributes: BubbleItemAttributes) var found = false for currentNodeItemValue in currentContentClassesPropertiesAndLayouts { - let currentNodeItem = currentNodeItemValue as (message: Message, type: AnyClass, supportsMosaic: Bool, currentLayout: (ChatMessageBubbleContentItem, ChatMessageItemLayoutConstants, ChatMessageBubblePreparePosition, Bool?, CGSize, CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void)))) + let currentNodeItem = currentNodeItemValue as (message: Message, type: AnyClass, supportsMosaic: Bool, index: Int?, currentLayout: (ChatMessageBubbleContentItem, ChatMessageItemLayoutConstants, ChatMessageBubblePreparePosition, Bool?, CGSize, CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void)))) - if currentNodeItem.type == contentNodeItem.type && currentNodeItem.message.stableId == contentNodeItem.message.stableId { + if currentNodeItem.type == contentNodeItem.type && currentNodeItem.index == contentNodeItem.bubbleAttributes.index && currentNodeItem.message.stableId == contentNodeItem.message.stableId { contentPropertiesAndPrepareLayouts.append((contentNodeItem.message, currentNodeItem.supportsMosaic, contentNodeItem.attributes, contentNodeItem.bubbleAttributes, currentNodeItem.currentLayout)) found = true break @@ -1791,11 +1833,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } if !found { let contentNode = (contentNodeItem.type as! ChatMessageBubbleContentNode.Type).init() + contentNode.index = contentNodeItem.bubbleAttributes.index contentPropertiesAndPrepareLayouts.append((contentNodeItem.message, contentNode.supportsMosaic, contentNodeItem.attributes, contentNodeItem.bubbleAttributes, contentNode.asyncLayoutContent())) if addedContentNodes == nil { addedContentNodes = [] } - addedContentNodes!.append((contentNodeItem.message, contentNodeItem.bubbleAttributes.isAttachment, contentNode)) + addedContentNodes!.append((contentNodeItem.message, contentNodeItem.bubbleAttributes.isAttachment, contentNode, contentNodeItem.bubbleAttributes.index)) } } @@ -2000,7 +2043,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let prepareContentPosition: ChatMessageBubblePreparePosition if let mosaicRange = mosaicRange, mosaicRange.contains(index) { - prepareContentPosition = .mosaic(top: .None(.None(.Incoming)), bottom: index == (mosaicRange.upperBound - 1) ? bottomPosition : .None(.None(.Incoming))) + let mosaicIndex = index - mosaicRange.lowerBound + prepareContentPosition = .mosaic(top: .None(.None(.Incoming)), bottom: index == (mosaicRange.upperBound - 1) ? bottomPosition : .None(.None(.Incoming)), index: mosaicIndex) } else { let refinedBottomPosition: ChatMessageBubbleRelativePosition if index == contentPropertiesAndPrepareLayouts.count - 1 { @@ -2245,7 +2289,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } if case .customChatContents = item.associatedData.subject { - } else if (mosaicRange.upperBound == contentPropertiesAndLayouts.count || contentPropertiesAndLayouts[contentPropertiesAndLayouts.count - 1].3.isAttachment) && !hasText { + } else if (mosaicRange.upperBound == contentPropertiesAndLayouts.count || contentPropertiesAndLayouts[contentPropertiesAndLayouts.count - 1].3.isAttachment) && (!hasText || item.message.invertMedia) { let message = item.content.firstMessage var edited = false @@ -2317,7 +2361,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI replyCount: dateReplies, isPinned: message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: message, isInline: item.associatedData.isInline), + canViewReactionList: canViewMessageReactionList(message: message), animationCache: item.controllerInteraction.presentationContext.animationCache, animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) @@ -2346,6 +2390,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var forwardSource: Peer? var forwardAuthorSignature: String? + var unlockButtonSizeApply: (CGSize, (Bool) -> ChatMessageUnlockMediaNode?) = (CGSize(), { _ in nil }) + var mediaInfoSizeApply: (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode?) = (CGSize(), { _ in nil }) + if displayHeader { let bubbleWidthInsets: CGFloat = mosaicRange == nil ? layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right : 0.0 if authorNameString != nil || inlineBotNameString != nil { @@ -2884,9 +2931,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var detachedContentNodesHeight: CGFloat = 0.0 var mosaicStatusOrigin: CGPoint? + var unlockButtonPosition: CGPoint? + var mediaInfoOrigin: CGPoint? for i in 0 ..< contentNodePropertiesAndFinalize.count { let (properties, position, finalize, contentGroupId, itemSelection) = contentNodePropertiesAndFinalize[i] - + if let position = position, case let .linear(top, bottom) = position { if case let .Neighbour(_, _, spacing) = top, case let .overlap(overlap) = spacing { currentContainerGroupOverlap = overlap @@ -2899,7 +2948,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let mosaicRange = mosaicRange, mosaicRange.contains(i), let (framesAndPositions, size) = calculatedGroupFramesAndSize { let mosaicIndex = i - mosaicRange.lowerBound - if mosaicIndex == 0 { + if mosaicIndex == 0 && i == 0 { if !headerSize.height.isZero { contentNodesHeight += 7.0 totalContentNodesHeight += 7.0 @@ -2911,12 +2960,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI contentNodeFramesPropertiesAndApply.append((contentNodeFrame, properties, true, apply)) if i == mosaicRange.upperBound - 1 { + unlockButtonPosition = CGPoint(x: size.width / 2.0, y: contentNodesHeight + size.height / 2.0) + mediaInfoOrigin = CGPoint(x: size.width, y: contentNodesHeight) + contentNodesHeight += size.height totalContentNodesHeight += size.height - + mosaicStatusOrigin = contentNodeFrame.bottomRight } } else { + let contentProperties = contentPropertiesAndLayouts[i].3 + if i == 0 && !headerSize.height.isZero { if contentGroupId == nil { contentNodesHeight += properties.headerSpacing @@ -2930,7 +2984,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if !contentContainerNodeFrames.isEmpty { overlapOffset = currentContainerGroupOverlap } - contentContainerNodeFrames.append((containerGroupId, CGRect(x: 0.0, y: headerSize.height + totalContentNodesHeight - contentNodesHeight - overlapOffset, width: maxContentWidth, height: contentNodesHeight), currentItemSelection, currentContainerGroupOverlap)) + let containerFrame = CGRect(x: 0.0, y: headerSize.height + totalContentNodesHeight - contentNodesHeight - overlapOffset, width: maxContentWidth, height: contentNodesHeight) + contentContainerNodeFrames.append((containerGroupId, containerFrame, currentItemSelection, currentContainerGroupOverlap)) + if !overlapOffset.isZero { totalContentNodesHeight -= currentContainerGroupOverlap } @@ -2945,7 +3001,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let contentNodeOriginY = contentNodesHeight - detachedContentNodesHeight let (size, apply) = finalize(maxContentWidth) - contentNodeFramesPropertiesAndApply.append((CGRect(origin: CGPoint(x: 0.0, y: contentNodeOriginY), size: size), properties, contentGroupId == nil, apply)) + let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentNodeOriginY), size: size) + contentNodeFramesPropertiesAndApply.append((containerFrame, properties, contentGroupId == nil, apply)) + + if contentProperties.neighborType == .media && unlockButtonPosition == nil { + unlockButtonPosition = containerFrame.center + mediaInfoOrigin = CGPoint(x: containerFrame.width, y: containerFrame.minY) + } contentNodesHeight += size.height totalContentNodesHeight += size.height @@ -2969,6 +3031,40 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI contentSize.height += totalContentNodesHeight + if let paidContent = item.message.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent, let media = paidContent.extendedMedia.first { + var isLocked = false + if case .preview = media { + isLocked = true + } else if item.presentationData.isPreview { + isLocked = true + } + if isLocked { + let sizeAndApply = unlockButtonLayout(ChatMessageUnlockMediaNode.Arguments( + presentationData: item.presentationData, + strings: item.presentationData.strings, + context: item.context, + controllerInteraction: item.controllerInteraction, + message: item.message, + media: paidContent, + constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer + )) + unlockButtonSizeApply = (sizeAndApply.0, { synchronousLoads in sizeAndApply.1(synchronousLoads) }) + } else { + let sizeAndApply = mediaInfoLayout(ChatMessageStarsMediaInfoNode.Arguments( + presentationData: item.presentationData, + context: item.context, + message: item.message, + media: paidContent, + constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer + )) + mediaInfoSizeApply = (sizeAndApply.0, { synchronousLoads in sizeAndApply.1(synchronousLoads) }) + } + } + var actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)? if let actionButtonsFinalize = actionButtonsFinalize { actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth) @@ -3099,6 +3195,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI contentContainerNodeFrames: contentContainerNodeFrames, mosaicStatusOrigin: mosaicStatusOrigin, mosaicStatusSizeAndApply: mosaicStatusSizeAndApply, + unlockButtonPosition: unlockButtonPosition, + unlockButtonSizeAndApply: unlockButtonSizeApply, + mediaInfoOrigin: mediaInfoOrigin, + mediaInfoSizeAndApply: mediaInfoSizeApply, needsShareButton: needsShareButton, needsTrButton: needTrButton, shareButtonOffset: shareButtonOffset, @@ -3148,12 +3248,16 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI replyInfoOriginY: CGFloat, removedContentNodeIndices: [Int]?, updatedContentNodeOrder: Bool, - addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode)]?, + addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode, Int?)]?, contentNodeMessagesAndClasses: [(Message, AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)], contentNodeFramesPropertiesAndApply: [(CGRect, ChatMessageBubbleContentProperties, Bool, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void)], contentContainerNodeFrames: [(UInt32, CGRect, Bool?, CGFloat)], mosaicStatusOrigin: CGPoint?, mosaicStatusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)?, + unlockButtonPosition: CGPoint?, + unlockButtonSizeAndApply: (CGSize, (Bool) -> ChatMessageUnlockMediaNode?), + mediaInfoOrigin: CGPoint?, + mediaInfoSizeAndApply: (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode?), needsShareButton: Bool, needsTrButton: Bool, shareButtonOffset: CGPoint?, @@ -3172,6 +3276,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI strongSelf.didChangeFromPendingToSent = true } + if case let .messageOptions(_, _, info) = item.associatedData.subject, case let .link(link) = info, link.isCentered { + strongSelf.wantsTrailingItemSpaceUpdates = true + } else { + strongSelf.wantsTrailingItemSpaceUpdates = false + } + let themeUpdated = strongSelf.appliedItem?.presentationData.theme.theme !== item.presentationData.theme.theme let previousContextFrame = strongSelf.mainContainerNode.frame strongSelf.mainContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) @@ -3212,7 +3322,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } else if !incoming { backgroundType = .outgoing(mergeType) } else { - backgroundType = .incoming(mergeType) + if case let .messageOptions(_, _, info) = item.associatedData.subject, case let .link(link) = info, link.isCentered { + backgroundType = .incoming(.Extracted) + } else if !item.presentationData.chatBubbleCorners.hasTails { + backgroundType = .incoming(.Extracted) + } else { + backgroundType = .incoming(mergeType) + } } let hasWallpaper = item.presentationData.theme.wallpaper.hasWallpaper if item.presentationData.theme.theme.forceSync { @@ -3942,7 +4058,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } if let addedContentNodes = addedContentNodes { - for (contentNodeMessage, isAttachment, contentNode) in addedContentNodes { + for (contentNodeMessage, isAttachment, contentNode, _ ) in addedContentNodes { updatedContentNodes.append(contentNode) let contextSourceNode: ContextExtractedContentContainingNode @@ -3974,18 +4090,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var sortedContentNodes: [ChatMessageBubbleContentNode] = [] outer: for contentItemValue in contentNodeMessagesAndClasses { - let contentItem = contentItemValue as (message: Message, type: AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes) - + let contentItem = contentItemValue as (message: Message, type: AnyClass, ChatMessageEntryAttributes, attributes: BubbleItemAttributes) if let addedContentNodes = addedContentNodes { - for (contentNodeMessage, _, contentNode) in addedContentNodes { - if type(of: contentNode) == contentItem.type && contentNodeMessage.stableId == contentItem.message.stableId { + for (contentNodeMessage, _, contentNode, index) in addedContentNodes { + if type(of: contentNode) == contentItem.type && index == contentItem.attributes.index && contentNodeMessage.stableId == contentItem.message.stableId { sortedContentNodes.append(contentNode) continue outer } } } for contentNode in updatedContentNodes { - if type(of: contentNode) == contentItem.type && contentNode.item?.message.stableId == contentItem.message.stableId { + if type(of: contentNode) == contentItem.type && contentNode.index == contentItem.attributes.index && contentNode.item?.message.stableId == contentItem.message.stableId { sortedContentNodes.append(contentNode) continue outer } @@ -4147,6 +4262,56 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI strongSelf.mosaicStatusNode = nil mosaicStatusNode.removeFromSupernode() } + + if let unlockButtonPosition { + let (size, apply) = unlockButtonSizeAndApply + var unlockButtonNodeAnimation = animation + if strongSelf.unlockButtonNode == nil { + unlockButtonNodeAnimation = .None + } + let unlockButtonNode = apply(strongSelf.unlockButtonNode != nil) + if unlockButtonNode !== strongSelf.unlockButtonNode { + strongSelf.unlockButtonNode?.removeFromSupernode() + strongSelf.unlockButtonNode = unlockButtonNode + if let unlockButtonNode { + strongSelf.clippingNode.addSubnode(unlockButtonNode) + } + } + let absoluteOrigin = unlockButtonPosition.offsetBy(dx: contentOrigin.x, dy: contentOrigin.y) + if let unlockButtonNode { + unlockButtonNodeAnimation.animator.updateFrame(layer: unlockButtonNode.layer, frame: CGRect(origin: CGPoint(x: floor(absoluteOrigin.x - size.width / 2.0), y: floor(absoluteOrigin.y - size.height / 2.0)), size: size), completion: nil) + } + } else if let unlockButtonNode = strongSelf.unlockButtonNode { + strongSelf.unlockButtonNode = nil + unlockButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + unlockButtonNode.removeFromSupernode() + }) + } + + if let mediaInfoOrigin { + let (size, apply) = mediaInfoSizeAndApply + var unlockButtonNodeAnimation = animation + if strongSelf.unlockButtonNode == nil { + unlockButtonNodeAnimation = .None + } + let mediaInfoNode = apply(strongSelf.mediaInfoNode != nil) + if mediaInfoNode !== strongSelf.mediaInfoNode { + strongSelf.mediaInfoNode?.removeFromSupernode() + strongSelf.mediaInfoNode = mediaInfoNode + if let mediaInfoNode { + strongSelf.clippingNode.addSubnode(mediaInfoNode) + } + } + let absoluteOrigin = mediaInfoOrigin.offsetBy(dx: contentOrigin.x, dy: contentOrigin.y) + if let mediaInfoNode { + unlockButtonNodeAnimation.animator.updateFrame(layer: mediaInfoNode.layer, frame: CGRect(origin: CGPoint(x: absoluteOrigin.x - size.width - 8.0, y: absoluteOrigin.y + 8.0), size: size), completion: nil) + } + } else if let mediaInfoNode = strongSelf.mediaInfoNode { + strongSelf.mediaInfoNode = nil + mediaInfoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + mediaInfoNode.removeFromSupernode() + }) + } if needsShareButton { if strongSelf.shareButtonNode == nil { @@ -4820,6 +4985,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let convertedLocation = self.view.convert(location, to: contentNode.view) let tapAction = contentNode.tapActionAtPoint(convertedLocation, gesture: gesture, isEstimating: false) + var rects: [CGRect] = [] + if let actionRects = tapAction.rects { + for rect in actionRects { + rects.append(rect.offsetBy(dx: contentNode.frame.minX, dy: contentNode.frame.minY)) + } + } + switch tapAction.content { case .none: if let item = self.item, self.backgroundNode.frame.contains(CGPoint(x: self.frame.width - location.x, y: location.y)), let tapMessage = self.item?.controllerInteraction.tapMessage { @@ -4863,13 +5035,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } case let .phone(number): return .action(InternalBubbleTapAction.Action({ [weak self] in - guard let self, let item = self.item, let contentNode = self.contextContentNodeForPhoneNumber(number) else { + guard let self, let item = self.item, let contentNode = self.contextContentNodeForLink(number, rects: rects) else { return } - - self.addSubnode(contentNode) - - item.controllerInteraction.openPhoneContextMenu(ChatControllerInteraction.OpenPhone(number: number, message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) + + item.controllerInteraction.longTap(.phone(number), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }, contextMenuOnLongPress: !tapAction.hasLongTapAction)) case let .peerMention(peerId, _, openProfile): return .action(InternalBubbleTapAction.Action { [weak self] in @@ -4938,8 +5108,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } case let .bankCard(number): if let item = self.item { - return .action(InternalBubbleTapAction.Action { - item.controllerInteraction.longTap(.bankCard(number), item.message) + return .action(InternalBubbleTapAction.Action { [weak self] in + guard let self, let contentNode = self.contextContentNodeForLink(number, rects: rects) else { + return + } + item.controllerInteraction.longTap(.bankCard(number), ChatControllerInteraction.LongTapParams(message: item.message, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) } case let .tooltip(text, node, rect): @@ -4987,7 +5160,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return nil case .longTap, .doubleTap, .secondaryTap: if let item = self.item, self.backgroundNode.frame.contains(location) { - let message = item.message +// let message = item.message if let threadInfoNode = self.threadInfoNode, self.item?.controllerInteraction.tapMessage == nil, threadInfoNode.frame.contains(location) { return .action(InternalBubbleTapAction.Action {}) @@ -5022,42 +5195,62 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI tapMessage = contentNode.item?.message } let tapAction = contentNode.tapActionAtPoint(convertedLocation, gesture: gesture, isEstimating: false) + var rects: [CGRect] = [] + if let actionRects = tapAction.rects { + for rect in actionRects { + rects.append(rect.offsetBy(dx: contentNode.frame.minX, dy: contentNode.frame.minY)) + } + } + switch tapAction.content { case .none, .ignore: break case let .url(url): if tapAction.hasLongTapAction { - return .action(InternalBubbleTapAction.Action({ - item.controllerInteraction.longTap(.url(url.url), message) + return .action(InternalBubbleTapAction.Action({ [weak self] in + let cleanUrl = url.url.replacingOccurrences(of: "mailto:", with: "") + guard let self, let contentNode = self.contextContentNodeForLink(cleanUrl, rects: rects) else { + return + } + item.controllerInteraction.longTap(.url(url.url), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }, contextMenuOnLongPress: false)) } else { disableDefaultPressAnimation = true } case let .phone(number): return .action(InternalBubbleTapAction.Action({ [weak self] in - guard let self, let item = self.item, let contentNode = self.contextContentNodeForPhoneNumber(number) else { + guard let self, let contentNode = self.contextContentNodeForLink(number, rects: rects) else { return } - - self.addSubnode(contentNode) - - item.controllerInteraction.openPhoneContextMenu(ChatControllerInteraction.OpenPhone(number: number, message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) + item.controllerInteraction.longTap(.phone(number), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }, contextMenuOnLongPress: !tapAction.hasLongTapAction)) case let .peerMention(peerId, mention, _): - return .action(InternalBubbleTapAction.Action { - item.controllerInteraction.longTap(.peerMention(peerId, mention), message) + return .action(InternalBubbleTapAction.Action { [weak self] in + guard let self, let contentNode = self.contextContentNodeForLink(mention, rects: rects) else { + return + } + item.controllerInteraction.longTap(.peerMention(peerId, mention), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) case let .textMention(name): - return .action(InternalBubbleTapAction.Action { - item.controllerInteraction.longTap(.mention(name), message) + return .action(InternalBubbleTapAction.Action { [weak self] in + guard let self, let contentNode = self.contextContentNodeForLink(name, rects: rects) else { + return + } + item.controllerInteraction.longTap(.mention(name), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) case let .botCommand(command): - return .action(InternalBubbleTapAction.Action { - item.controllerInteraction.longTap(.command(command), message) + return .action(InternalBubbleTapAction.Action { [weak self] in + guard let self, let contentNode = self.contextContentNodeForLink(command, rects: rects) else { + return + } + item.controllerInteraction.longTap(.command(command), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) case let .hashtag(_, hashtag): - return .action(InternalBubbleTapAction.Action { - item.controllerInteraction.longTap(.hashtag(hashtag), message) + return .action(InternalBubbleTapAction.Action { [weak self] in + guard let self, let contentNode = self.contextContentNodeForLink(hashtag, rects: rects) else { + return + } + item.controllerInteraction.longTap(.hashtag(hashtag), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) case .instantPage: break @@ -5071,13 +5264,19 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI break case let .timecode(timecode, text): if let mediaMessage = mediaMessage { - return .action(InternalBubbleTapAction.Action { - item.controllerInteraction.longTap(.timecode(timecode, text), mediaMessage) + return .action(InternalBubbleTapAction.Action { [weak self] in + guard let self, let contentNode = self.contextContentNodeForLink(text, rects: rects) else { + return + } + item.controllerInteraction.longTap(.timecode(timecode, text), ChatControllerInteraction.LongTapParams(message: mediaMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) } case let .bankCard(number): - return .action(InternalBubbleTapAction.Action { - item.controllerInteraction.longTap(.bankCard(number), message) + return .action(InternalBubbleTapAction.Action { [weak self] in + guard let self, let contentNode = self.contextContentNodeForLink(number, rects: rects) else { + return + } + item.controllerInteraction.longTap(.bankCard(number), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) case .tooltip: break @@ -5112,7 +5311,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return nil } - private func contextContentNodeForPhoneNumber(_ number: String) -> ContextExtractedContentContainingNode? { + private func contextContentNodeForLink(_ link: String, rects: [CGRect]?) -> ContextExtractedContentContainingNode? { guard let item = self.item else { return nil } @@ -5121,8 +5320,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let incoming = item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) let textNode = ImmediateTextNode() - textNode.attributedText = NSAttributedString(string: number, font: Font.regular(item.presentationData.fontSize.baseDisplaySize), textColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.linkTextColor : item.presentationData.theme.theme.chat.message.outgoing.linkTextColor) - let textSize = textNode.updateLayout(CGSize(width: 1000.0, height: 100.0)) + textNode.maximumNumberOfLines = 2 + textNode.attributedText = NSAttributedString(string: link, font: Font.regular(item.presentationData.fontSize.baseDisplaySize), textColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.linkTextColor : item.presentationData.theme.theme.chat.message.outgoing.linkTextColor) + let textSize = textNode.updateLayout(CGSize(width: self.bounds.width - 32.0, height: 100.0)) let backgroundNode = ASDisplayNode() backgroundNode.backgroundColor = (incoming ? item.presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill : item.presentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper.fill).first ?? .black @@ -5135,13 +5335,20 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI textNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: textSize) backgroundNode.addSubnode(textNode) - containingNode.frame = CGRect(origin: CGPoint(x: self.backgroundNode.frame.minX + 3.0, y: 1.0), size: CGSize(width: backgroundSize.width, height: backgroundSize.height + 20.0)) + var origin = CGPoint(x: self.backgroundNode.frame.minX + 3.0, y: 1.0) + if let rect = rects?.first { + origin = rect.origin + } + + containingNode.frame = CGRect(origin: origin, size: CGSize(width: backgroundSize.width, height: backgroundSize.height + 20.0)) containingNode.contentNode.frame = CGRect(origin: .zero, size: backgroundSize) containingNode.contentRect = CGRect(origin: .zero, size: backgroundSize) containingNode.contentNode.addSubnode(backgroundNode) containingNode.contentNode.alpha = 0.0 + self.addSubnode(containingNode) + return containingNode } @@ -5275,6 +5482,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } override public func updateHiddenMedia() { + var hasHiddenMediaInfo = false var hasHiddenMosaicStatus = false var hasHiddenBackground = false if let item = self.item { @@ -5287,6 +5495,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let mosaicStatusNode = self.mosaicStatusNode, mosaicStatusNode.frame.intersects(contentNode.frame) { hasHiddenMosaicStatus = true } + if let mediaInfoNode = self.mediaInfoNode, mediaInfoNode.frame.intersects(contentNode.frame) { + hasHiddenMediaInfo = true + } } } } @@ -5303,6 +5514,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } + if let mediaInfoNode = self.mediaInfoNode { + if mediaInfoNode.alpha.isZero != hasHiddenMediaInfo { + if hasHiddenMediaInfo { + mediaInfoNode.alpha = 0.0 + } else { + mediaInfoNode.alpha = 1.0 + mediaInfoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + } + self.backgroundNode.isHidden = hasHiddenBackground self.backgroundWallpaperNode.isHidden = hasHiddenBackground } @@ -5916,6 +6138,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI item.controllerInteraction.openMessageContextMenu(item.message, true, self, subFrame, nil, nil) } + override public func makeProgress() -> Promise? { + return self.unlockButtonNode?.makeProgress() + } + override public func targetReactionView(value: MessageReaction.Reaction) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift index bde09fc8786..e6156462fcf 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift @@ -302,7 +302,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline), + canViewReactionList: canViewMessageReactionList(message: item.topMessage), animationCache: item.controllerInteraction.presentationContext.animationCache, animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift index 439f7b0b531..ce5c3d34b38 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift @@ -356,6 +356,10 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { selectedBackground: themeColors.reactionActiveBackground.argb, deselectedForeground: themeColors.reactionInactiveForeground.argb, selectedForeground: themeColors.reactionActiveForeground.argb, + deselectedStarsBackground: themeColors.reactionStarsInactiveBackground.argb, + selectedStarsBackground: themeColors.reactionStarsActiveBackground.argb, + deselectedStarsForeground: themeColors.reactionStarsInactiveForeground.argb, + selectedStarsForeground: themeColors.reactionStarsActiveForeground.argb, extractedBackground: arguments.presentationData.theme.theme.contextMenu.backgroundColor.argb, extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb, extractedSelectedForeground: arguments.presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : arguments.presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb, @@ -370,6 +374,10 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { selectedBackground: themeColors.reactionActiveBackground.argb, deselectedForeground: themeColors.reactionInactiveForeground.argb, selectedForeground: themeColors.reactionActiveForeground.argb, + deselectedStarsBackground: themeColors.reactionStarsInactiveBackground.argb, + selectedStarsBackground: themeColors.reactionStarsActiveBackground.argb, + deselectedStarsForeground: themeColors.reactionStarsInactiveForeground.argb, + selectedStarsForeground: themeColors.reactionStarsActiveForeground.argb, extractedBackground: arguments.presentationData.theme.theme.contextMenu.backgroundColor.argb, extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb, extractedSelectedForeground: arguments.presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : arguments.presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb, @@ -897,7 +905,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { let canViewReactionList = arguments.canViewReactionList item.node.view.activateAfterCompletion = !canViewReactionList item.node.view.activated = { [weak itemNode] gesture, _ in - guard let strongSelf = self, canViewReactionList else { + guard let strongSelf = self else { return } guard let itemNode = itemNode else { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift index 62c4f1e6106..a2e2b5deef1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift @@ -449,7 +449,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline), + canViewReactionList: canViewMessageReactionList(message: item.topMessage), animationCache: item.controllerInteraction.presentationContext.animationCache, animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift index 466041cbebc..20bbc99a732 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift @@ -532,7 +532,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline), + canViewReactionList: canViewMessageReactionList(message: item.topMessage), animationCache: item.controllerInteraction.presentationContext.animationCache, animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index 02ee9cf6c89..59b4284e66b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -1490,7 +1490,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco reactionButtonsNode.offset(value: value, animationCurve: animationCurve, duration: duration) } } - + override public func targetReactionView(value: MessageReaction.Reaction) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index c11fa901224..ecb5d216f45 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -1095,7 +1095,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { replyCount: dateReplies, isPinned: arguments.isPinned && !arguments.associatedData.isInPinnedListMode, hasAutoremove: arguments.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: arguments.topMessage, isInline: arguments.associatedData.isInline), + canViewReactionList: canViewMessageReactionList(message: arguments.topMessage), animationCache: arguments.controllerInteraction.presentationContext.animationCache, animationRenderer: arguments.controllerInteraction.presentationContext.animationRenderer )) @@ -1410,7 +1410,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } let waveformView: ComponentHostView - let waveformTransition: Transition + let waveformTransition: ComponentTransition if let current = strongSelf.waveformView { waveformView = current switch animation.transition { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index 6e5ebdf780c..d119a50ed4e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -473,7 +473,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { updatedPlaybackStatus = combineLatest(messageFileMediaResourceStatus(context: item.context, file: updatedFile, message: EngineMessage(item.message), isRecentActions: item.associatedData.isRecentActions), item.context.account.pendingMessageManager.pendingMessageStatus(item.message.id) |> map { $0.0 }) |> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in if let pendingStatus = pendingStatus { - var progress = pendingStatus.progress + var progress = pendingStatus.progress.progress if pendingStatus.isRunning { progress = max(progress, 0.27) } @@ -585,7 +585,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline), + canViewReactionList: canViewMessageReactionList(message: item.topMessage), animationCache: item.controllerInteraction.presentationContext.animationCache, animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD index 9ef1bf2bc35..21b6897eac3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD @@ -39,6 +39,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", "//submodules/TelegramUI/Components/WallpaperPreviewMedia", + "//submodules/TelegramUI/Components/TextNodeWithEntities", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index b497f7abde4..6f3e5a04347 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -29,6 +29,7 @@ import ChatMessageDateAndStatusNode import ChatHistoryEntry import ChatMessageItemCommon import WallpaperPreviewMedia +import TextNodeWithEntities private struct FetchControls { let fetch: (Bool) -> Void @@ -213,12 +214,14 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { } } } + private let context: AccountContext + private let blurredImageNode: TransformImageNode private let dustNode: MediaDustNode fileprivate let buttonNode: HighlightTrackingButtonNode private let highlightedBackgroundNode: ASDisplayNode private let iconNode: ASImageNode - private let textNode: ImmediateTextNode + private let textNode: ImmediateTextNodeWithEntities private var maskView: UIView? private var maskLayer: CAShapeLayer? @@ -227,7 +230,9 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { var isRevealed = false var tapped: () -> Void = {} - init(hasImageOverlay: Bool, icon: Icon, enableAnimations: Bool) { + init(context: AccountContext, hasImageOverlay: Bool, icon: Icon?, enableAnimations: Bool) { + self.context = context + self.blurredImageNode = TransformImageNode() self.blurredImageNode.contentAnimations = [] @@ -244,9 +249,9 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { self.iconNode = ASImageNode() self.iconNode.displaysAsynchronously = false - self.iconNode.image = icon.image + self.iconNode.image = icon?.image - self.textNode = ImmediateTextNode() + self.textNode = ImmediateTextNodeWithEntities() self.textNode.isUserInteractionEnabled = false super.init() @@ -366,16 +371,28 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { self.buttonNode.isHidden = false self.textNode.isHidden = false - self.textNode.attributedText = NSAttributedString(string: text, font: Font.semibold(14.0), textColor: .white, paragraphAlignment: .center) + self.textNode.arguments = TextNodeWithEntities.Arguments(context: self.context, cache: self.context.animationCache, renderer: self.context.animationRenderer, placeholderColor: .clear, attemptSynchronous: true) + + let string = NSMutableAttributedString(string: text, font: Font.semibold(15.0), textColor: .white) + if let range = string.string.range(of: "⭐️") { + string.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: string.string)) + string.addAttribute(.baselineOffset, value: 0.5, range: NSRange(range, in: string.string)) + } + + self.textNode.attributedText = string let textSize = self.textNode.updateLayout(size) - if let iconSize = self.iconNode.image?.size { - let contentSize = CGSize(width: iconSize.width + textSize.width + spacing + padding * 2.0, height: 32.0) - self.buttonNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - contentSize.width) / 2.0), y: floorToScreenPixels((size.height - contentSize.height) / 2.0)), size: contentSize) - self.highlightedBackgroundNode.frame = CGRect(origin: .zero, size: contentSize) + let iconSize = self.iconNode.image?.size ?? .zero - self.iconNode.frame = CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((contentSize.height - iconSize.height) / 2.0) + 1.0 - UIScreenPixel), size: iconSize) - self.textNode.frame = CGRect(origin: CGPoint(x: self.iconNode.frame.maxX + spacing, y: floorToScreenPixels((contentSize.height - textSize.height) / 2.0)), size: textSize) + var contentSize = CGSize(width: textSize.width + padding * 2.0, height: 32.0) + if iconSize.width > 0.0 { + contentSize.width += iconSize.width + spacing } + + self.buttonNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - contentSize.width) / 2.0), y: floorToScreenPixels((size.height - contentSize.height) / 2.0)), size: contentSize) + self.highlightedBackgroundNode.frame = CGRect(origin: .zero, size: contentSize) + + self.iconNode.frame = CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((contentSize.height - iconSize.height) / 2.0) + 1.0 - UIScreenPixel), size: iconSize) + self.textNode.frame = CGRect(origin: CGPoint(x: contentSize.width - padding - textSize.width, y: floorToScreenPixels((contentSize.height - textSize.height) / 2.0)), size: textSize) } var leftOffset: CGFloat = 0.0 @@ -395,6 +412,14 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { } } +private func selectStoryMedia(item: Stories.Item, preferredHighQuality: Bool) -> Media? { + if !preferredHighQuality, let alternativeMedia = item.alternativeMedia { + return alternativeMedia + } else { + return item.media + } +} + public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitionNode { private let pinchContainerNode: PinchSourceContainerNode private let imageNode: TransformImageNode @@ -420,11 +445,13 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr private var message: Message? private var attributes: ChatMessageEntryAttributes? private var media: Media? + private var mediaIndex: Int? private var themeAndStrings: (PresentationTheme, PresentationStrings, String, Bool)? private var sizeCalculation: InteractiveMediaNodeSizeCalculation? private var wideLayout: Bool? private var automaticDownload: InteractiveMediaNodeAutodownloadMode? public var automaticPlayback: Bool? + private var preferredStoryHighQuality: Bool = false private let statusDisposable = MetaDisposable() private let fetchControls = Atomic(value: nil) @@ -651,6 +678,13 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } else if let media = media as? TelegramMediaImage, let resource = largestImageRepresentation(media.representations)?.resource { messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: media, resource: resource) } + if let alternativeMedia = item.alternativeMedia { + if let media = alternativeMedia as? TelegramMediaFile { + messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: media) + } else if let media = alternativeMedia as? TelegramMediaImage, let resource = largestImageRepresentation(media.representations)?.resource { + messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: media, resource: resource) + } + } } } } @@ -690,8 +724,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } if let storyMedia = media as? TelegramMediaStory, let storyItem = self.message?.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) { - if case let .item(item) = storyItem, let mediaValue = item.media { - media = mediaValue + if case let .item(item) = storyItem, let _ = item.media { + media = selectStoryMedia(item: item, preferredHighQuality: self.preferredStoryHighQuality) } } @@ -706,9 +740,10 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } else { if let invoice = self.media as? TelegramMediaInvoice, let _ = invoice.extendedMedia { self.activateLocalContent(.default) + } else if let _ = self.media as? TelegramMediaPaidContent { + self.activateLocalContent(.default) } else if let storyMedia = media as? TelegramMediaStory, let storyItem = self.message?.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) { - if case let .item(item) = storyItem, let mediaValue = item.media { - let _ = mediaValue + if case let .item(item) = storyItem, let _ = item.media { self.activateLocalContent(.default) } } else { @@ -719,7 +754,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } - public func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { + public func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ mediaIndex: Int?, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { let currentMessage = self.message let currentMedia = self.media let imageLayout = self.imageNode.asyncLayout() @@ -733,7 +768,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr let currentAutomaticDownload = self.automaticDownload let currentAutomaticPlayback = self.automaticPlayback - return { [weak self] context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext in + return { [weak self] context, presentationData, dateTimeFormat, message, associatedData, attributes, media, mediaIndex, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext in let _ = peerType var nativeSize: CGSize @@ -767,15 +802,23 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr var maxHeight = layoutConstants.image.maxDimensions.height var isStory = false + let _ = isStory + var additionalWidthConstrainment = false var unboundSize: CGSize - if let _ = media as? TelegramMediaStory { + if let story = media as? TelegramMediaStory { if message.media.contains(where: { $0 is TelegramMediaWebpage }) { additionalWidthConstrainment = true unboundSize = CGSize(width: 174.0, height: 239.0) } else { unboundSize = CGSize(width: 1080, height: 1920) } + + if let storyItem = message.associatedStories[story.storyId]?.get(Stories.StoredItem.self), case let .item(item) = storyItem, let media = item.media { + if let file = media as? TelegramMediaFile { + isInlinePlayableVideo = file.isVideo && !isSecretMedia + } + } } else if let image = media as? TelegramMediaImage, let dimensions = largestImageRepresentation(image.representations)?.dimensions { unboundSize = CGSize(width: max(10.0, floor(dimensions.cgSize.width * 0.5)), height: max(10.0, floor(dimensions.cgSize.height * 0.5))) } else if let file = media as? TelegramMediaFile, var dimensions = file.dimensions { @@ -832,8 +875,18 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr case .color, .gradient, .emoticon: unboundSize = CGSize(width: 128.0, height: 128.0) } - } else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia { - switch extendedMedia { + } else { + var extendedMedia: TelegramExtendedMedia? + if let invoice = media as? TelegramMediaInvoice, let selectedMedia = invoice.extendedMedia { + extendedMedia = selectedMedia + } else if let paidContent = media as? TelegramMediaPaidContent { + let selectedMediaIndex = mediaIndex ?? 0 + if selectedMediaIndex < paidContent.extendedMedia.count { + extendedMedia = paidContent.extendedMedia[selectedMediaIndex] + } + } + if let extendedMedia { + switch extendedMedia { case let .preview(dimensions, _, _): if let dimensions = dimensions { unboundSize = CGSize(width: max(10.0, floor(dimensions.cgSize.width * 0.5)), height: max(10.0, floor(dimensions.cgSize.height * 0.5))) @@ -867,9 +920,10 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } else { unboundSize = CGSize(width: 54.0, height: 54.0) } + } + } else { + unboundSize = CGSize(width: 54.0, height: 54.0) } - } else { - unboundSize = CGSize(width: 54.0, height: 54.0) } switch sizeCalculation { @@ -917,7 +971,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr replyCount: dateAndStatus.dateReplies, isPinned: dateAndStatus.isPinned, hasAutoremove: message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: message, isInline: associatedData.isInline), + canViewReactionList: canViewMessageReactionList(message: message), animationCache: presentationContext.animationCache, animationRenderer: presentationContext.animationRenderer )) @@ -1077,13 +1131,34 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr if mediaUpdated || isSendingUpdated || automaticPlaybackUpdated { var media = media - if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia { + + var extendedMedia: TelegramExtendedMedia? + if let invoice = media as? TelegramMediaInvoice, let selectedMedia = invoice.extendedMedia { + extendedMedia = selectedMedia + } else if let paidContent = media as? TelegramMediaPaidContent { + let selectedMediaIndex = mediaIndex ?? 0 + if selectedMediaIndex < paidContent.extendedMedia.count { + extendedMedia = paidContent.extendedMedia[selectedMediaIndex] + } + } + + if let extendedMedia { switch extendedMedia { - case let .preview(_, immediateThumbnailData, _): - let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - media = thumbnailMedia - case let .full(fullMedia): + case let .preview(_, immediateThumbnailData, _): + let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) + media = thumbnailMedia + case let .full(fullMedia): + if presentationData.isPreview { + if let image = fullMedia as? TelegramMediaImage { + let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) + media = thumbnailMedia + } else if let video = fullMedia as? TelegramMediaFile { + let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: video.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) + media = thumbnailMedia + } + } else { media = fullMedia + } } } @@ -1097,7 +1172,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr replaceAnimatedStickerNode = true } - if let storyItem = message.associatedStories[story.storyId]?.get(Stories.StoredItem.self), case let .item(item) = storyItem, let media = item.media { + if let storyItem = message.associatedStories[story.storyId]?.get(Stories.StoredItem.self), case let .item(item) = storyItem, let media = selectStoryMedia(item: item, preferredHighQuality: associatedData.preferredStoryHighQuality) { if let image = media as? TelegramMediaImage { if hasCurrentVideoNode { replaceVideoNode = true @@ -1161,7 +1236,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr uploading = true } - if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !isStory && !uploading { + if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !uploading { updateVideoFile = file if hasCurrentVideoNode { if let currentFile = currentMedia as? TelegramMediaFile { @@ -1297,7 +1372,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr uploading = true } - if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !isStory && !uploading { + if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !uploading { updateVideoFile = file if hasCurrentVideoNode { if let currentFile = currentMedia as? TelegramMediaFile { @@ -1406,12 +1481,21 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr var isExtendedMedia = false if statusUpdated { var media = media - if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { + var extendedMedia: TelegramExtendedMedia? + if let invoice = media as? TelegramMediaInvoice, let selectedMedia = invoice.extendedMedia { + extendedMedia = selectedMedia + } else if let paidContent = media as? TelegramMediaPaidContent { + let selectedMediaIndex = mediaIndex ?? 0 + if selectedMediaIndex < paidContent.extendedMedia.count { + extendedMedia = paidContent.extendedMedia[selectedMediaIndex] + } + } + if let extendedMedia, case let .full(fullMedia) = extendedMedia, !presentationData.isPreview { isExtendedMedia = true media = fullMedia } if let storyMedia = media as? TelegramMediaStory, let storyItem = message.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) { - if case let .item(item) = storyItem, let mediaValue = item.media { + if case let .item(item) = storyItem, let mediaValue = selectStoryMedia(item: item, preferredHighQuality: associatedData.preferredStoryHighQuality) { media = mediaValue } } @@ -1421,7 +1505,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr updatedStatusSignal = combineLatest(chatMessagePhotoStatus(context: context, messageId: message.id, photoReference: .message(message: MessageReference(message), media: image)), context.account.pendingMessageManager.pendingMessageStatus(message.id) |> map { $0.0 }) |> map { resourceStatus, pendingStatus -> (MediaResourceStatus, MediaResourceStatus?) in if let pendingStatus = pendingStatus { - let adjustedProgress = max(pendingStatus.progress, 0.027) + var progress: Float = pendingStatus.progress.progress + if let id = media.id, let mediaProgress = pendingStatus.progress.mediaProgress[id] { + progress = mediaProgress + } + let adjustedProgress = max(progress, 0.027) return (.Fetching(isActive: pendingStatus.isRunning, progress: adjustedProgress), resourceStatus) } else { return (resourceStatus, nil) @@ -1437,7 +1525,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr updatedStatusSignal = combineLatest(messageMediaFileStatus(context: context, messageId: message.id, file: file, adjustForVideoThumbnail: true), context.account.pendingMessageManager.pendingMessageStatus(message.id) |> map { $0.0 }) |> map { resourceStatus, pendingStatus -> (MediaResourceStatus, MediaResourceStatus?) in if let pendingStatus = pendingStatus { - let adjustedProgress = max(pendingStatus.progress, 0.027) + var progress: Float = pendingStatus.progress.progress + if let id = media.id, let mediaProgress = pendingStatus.progress.mediaProgress[id] { + progress = mediaProgress + } + let adjustedProgress = max(progress, 0.027) return (.Fetching(isActive: pendingStatus.isRunning, progress: adjustedProgress), resourceStatus) } else { return (resourceStatus, nil) @@ -1469,11 +1561,13 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr strongSelf.message = message strongSelf.attributes = attributes strongSelf.media = media + strongSelf.mediaIndex = mediaIndex strongSelf.wideLayout = wideLayout strongSelf.themeAndStrings = (presentationData.theme.theme, presentationData.strings, dateTimeFormat.decimalSeparator, presentationData.isPreview) strongSelf.sizeCalculation = sizeCalculation strongSelf.automaticPlayback = automaticPlayback strongSelf.automaticDownload = automaticDownload + strongSelf.preferredStoryHighQuality = associatedData.preferredStoryHighQuality if let previousArguments = strongSelf.currentImageArguments { if previousArguments.imageSize == arguments.imageSize { @@ -1707,11 +1801,20 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr let _ = strongSelf.fetchControls.swap(updatedFetchControls) var media = media - if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { + var extendedMedia: TelegramExtendedMedia? + if let invoice = media as? TelegramMediaInvoice, let selectedMedia = invoice.extendedMedia { + extendedMedia = selectedMedia + } else if let paidContent = media as? TelegramMediaPaidContent { + let selectedMediaIndex = mediaIndex ?? 0 + if selectedMediaIndex < paidContent.extendedMedia.count { + extendedMedia = paidContent.extendedMedia[selectedMediaIndex] + } + } + if let extendedMedia, case let .full(fullMedia) = extendedMedia { media = fullMedia } if let storyMedia = media as? TelegramMediaStory, let storyItem = message.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) { - if case let .item(item) = storyItem, let mediaValue = item.media { + if case let .item(item) = storyItem, let mediaValue = selectStoryMedia(item: item, preferredHighQuality: associatedData.preferredStoryHighQuality) { media = mediaValue } } @@ -1812,11 +1915,14 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr var game: TelegramMediaGame? var webpage: TelegramMediaWebpage? var invoice: TelegramMediaInvoice? + var paidContent: TelegramMediaPaidContent? for media in message.media { if let media = media as? TelegramMediaWebpage { webpage = media } else if let media = media as? TelegramMediaInvoice { invoice = media + } else if let media = media as? TelegramMediaPaidContent { + paidContent = media } else if let media = media as? TelegramMediaGame { game = media } else if let _ = media as? TelegramMediaStory { @@ -1984,11 +2090,20 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr let formatting = DataSizeStringFormatting(strings: strings, decimalSeparator: decimalSeparator) var media = self.media - if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { + var extendedMedia: TelegramExtendedMedia? + if let invoice = media as? TelegramMediaInvoice, let selectedMedia = invoice.extendedMedia { + extendedMedia = selectedMedia + } else if let paidContent = media as? TelegramMediaPaidContent { + let selectedMediaIndex = self.mediaIndex ?? 0 + if selectedMediaIndex < paidContent.extendedMedia.count { + extendedMedia = paidContent.extendedMedia[selectedMediaIndex] + } + } + if let extendedMedia, case let .full(fullMedia) = extendedMedia { media = fullMedia } if let storyMedia = media as? TelegramMediaStory, let storyItem = message.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) { - if case let .item(item) = storyItem, let mediaValue = item.media { + if case let .item(item) = storyItem, let mediaValue = selectStoryMedia(item: item, preferredHighQuality: self.preferredStoryHighQuality) { media = mediaValue } } @@ -2096,7 +2211,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr state = .staticTimeout } else if let file = media as? TelegramMediaFile, !file.isVideoSticker { let isInlinePlayableVideo = file.isVideo && !isSecretMedia && (self.automaticPlayback ?? false) - if (!isInlinePlayableVideo || isStory) && file.isVideo { + if (!isInlinePlayableVideo) && file.isVideo { state = .play(messageTheme.mediaOverlayControlColors.foregroundColor) } else { state = .none @@ -2248,9 +2363,28 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr badgeNode.removeFromSupernode() } - var icon: ExtendedMediaOverlayNode.Icon = .lock + var icon: ExtendedMediaOverlayNode.Icon? var displaySpoiler = false - if let invoice = invoice, let extendedMedia = invoice.extendedMedia, case .preview = extendedMedia { + + var extendedMedia: TelegramExtendedMedia? + if let invoice, let selectedMedia = invoice.extendedMedia { + extendedMedia = selectedMedia + } else if let paidContent { + let selectedMediaIndex = self.mediaIndex ?? 0 + if selectedMediaIndex < paidContent.extendedMedia.count { + extendedMedia = paidContent.extendedMedia[selectedMediaIndex] + } + } + + if let extendedMedia, case .preview = extendedMedia { + if let invoice, invoice.currency != "XTR" { + icon = .lock + } + displaySpoiler = true + } else if let _ = extendedMedia, isPreview { + if let invoice, invoice.currency != "XTR" { + icon = .lock + } displaySpoiler = true } else if message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) { displaySpoiler = true @@ -2262,9 +2396,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } if displaySpoiler { - if self.extendedMediaOverlayNode == nil { - let enableAnimations = (self.context?.sharedContext.energyUsageSettings.fullTranslucency ?? true) && !isPreview - let extendedMediaOverlayNode = ExtendedMediaOverlayNode(hasImageOverlay: !isSecretMedia, icon: icon, enableAnimations: enableAnimations) + if self.extendedMediaOverlayNode == nil, let context = self.context { + let enableAnimations = context.sharedContext.energyUsageSettings.fullTranslucency && !isPreview + let extendedMediaOverlayNode = ExtendedMediaOverlayNode(context: context, hasImageOverlay: !isSecretMedia, icon: icon, enableAnimations: enableAnimations) extendedMediaOverlayNode.tapped = { [weak self] in guard let self else { return @@ -2296,7 +2430,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr var viewText: String = "" if message.isAgeRestricted() { - //TODO: localize + //TODO:localize viewText = "18+ Content" } else { outer: for attribute in message.attributes { @@ -2340,12 +2474,12 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr self.extendedMediaOverlayNode?.reveal(animated: true) } - public static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))) { + public static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ mediaIndex: Int?, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))) { let currentAsyncLayout = node?.asyncLayout() - return { context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext in + return { context, presentationData, dateTimeFormat, message, associatedData, attributes, media, mediaIndex, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext in var imageNode: ChatMessageInteractiveMediaNode - var imageLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) + var imageLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ mediaIndex: Int?, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { imageNode = node @@ -2355,7 +2489,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr imageLayout = imageNode.asyncLayout() } - let (unboundSize, initialWidth, continueLayout) = imageLayout(context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext) + let (unboundSize, initialWidth, continueLayout) = imageLayout(context, presentationData, dateTimeFormat, message, associatedData, attributes, media, mediaIndex, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext) return (unboundSize, initialWidth, { constrainedSize, automaticPlayback, wideLayout, corners in let (finalWidth, finalLayout) = continueLayout(constrainedSize, automaticPlayback, wideLayout, corners) @@ -2521,12 +2655,12 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } public func ignoreTapActionAtPoint(_ point: CGPoint) -> Bool { - if let extendedMediaOverlayNode = self.extendedMediaOverlayNode { - let convertedPoint = self.view.convert(point, to: extendedMediaOverlayNode.view) - if extendedMediaOverlayNode.buttonNode.frame.contains(convertedPoint) { - return true - } - } +// if let extendedMediaOverlayNode = self.extendedMediaOverlayNode { +// let convertedPoint = self.view.convert(point, to: extendedMediaOverlayNode.view) +// if extendedMediaOverlayNode.buttonNode.frame.contains(convertedPoint) { +// return true +// } +// } return false } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift index 2a856c79054..9cf52317b88 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift @@ -16,8 +16,12 @@ public enum ChatMessageItemContent: Sequence { case group(messages: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)]) public func effectivelyIncoming(_ accountPeerId: PeerId, associatedData: ChatMessageItemAssociatedData? = nil) -> Bool { - if let subject = associatedData?.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { - return false + if let subject = associatedData?.subject, case let .messageOptions(_, _, info) = subject { + if case .forward = info { + return false + } else if case let .link(link) = info { + return link.isCentered + } } switch self { case let .message(message, _, _, _, _): diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift index bcf039e7bdd..e634ace7835 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift @@ -161,7 +161,7 @@ public struct ChatMessageItemLayoutConstants { } } -public func canViewMessageReactionList(message: Message, isInline: Bool) -> Bool { +public func canViewMessageReactionList(message: Message) -> Bool { var found = false var canViewList = false for attribute in message.attributes { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift index d818c687b8d..b8ef21ddf06 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift @@ -851,7 +851,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { item.controllerInteraction.activateSwitchInline(peerId, "@\(addressName) \(query)", peerTypes) } case .payment: - item.controllerInteraction.openCheckoutOrReceipt(item.message.id) + item.controllerInteraction.openCheckoutOrReceipt(item.message.id, nil) case let .urlAuth(url, buttonId): item.controllerInteraction.requestMessageActionUrlAuth(url, .message(id: item.message.id, buttonId: buttonId)) case .setupPoll: @@ -875,7 +875,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { if let item = self.item { switch button.action { case let .url(url): - item.controllerInteraction.longTap(.url(url), item.message) + item.controllerInteraction.longTap(.url(url), ChatControllerInteraction.LongTapParams(message: item.message)) default: break } @@ -885,6 +885,10 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { open func openMessageContextMenu() { } + open func makeProgress() -> Promise? { + return nil + } + open func targetReactionView(value: MessageReaction.Reaction) -> UIView? { return nil } @@ -990,12 +994,14 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { additionalAnimationNode = DirectAnimatedStickerNode() effectiveScale = 1.4 #else - if "".isEmpty { + additionalAnimationNode = DirectAnimatedStickerNode() + effectiveScale = 1.4 + /*if "".isEmpty { additionalAnimationNode = DirectAnimatedStickerNode() effectiveScale = 1.4 } else { additionalAnimationNode = LottieMetalAnimatedStickerNode() - } + }*/ #endif additionalAnimationNode.updateLayout(size: animationSize) additionalAnimationNode.setup(source: source, width: Int(animationSize.width * effectiveScale), height: Int(animationSize.height * effectiveScale), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix)) @@ -1009,7 +1015,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) } - let decorationNode = transitionNode.add(decorationView: additionalAnimationNode.view, itemNode: self) + let decorationNode = transitionNode.add(decorationView: additionalAnimationNode.view, itemNode: self, aboveEverything: true) additionalAnimationNode.completed = { [weak self, weak decorationNode, weak transitionNode] _ in guard let decorationNode = decorationNode else { return diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift index fc3c136f6fe..0455a1c4a5c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift @@ -660,10 +660,10 @@ private final class ChannelItemComponent: Component { if highlighted { self.contextContainer.layer.removeAnimation(forKey: "sublayerTransform") - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) transition.setScale(layer: self.contextContainer.layer, scale: topScale) } else { - let transition = Transition(animation: .none) + let transition = ComponentTransition(animation: .none) transition.setScale(layer: self.contextContainer.layer, scale: 1.0) self.contextContainer.layer.animateScale(from: topScale, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false) @@ -691,7 +691,7 @@ private final class ChannelItemComponent: Component { } } - func update(component: ChannelItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChannelItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component self.state = state @@ -880,7 +880,7 @@ private final class ChannelItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -1024,7 +1024,7 @@ final class ChannelListPanelComponent: Component { } } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let itemLayout = self.itemLayout else { return } @@ -1123,7 +1123,7 @@ final class ChannelListPanelComponent: Component { } } - func update(component: ChannelListPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChannelListPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let itemLayout = ItemLayout( @@ -1159,7 +1159,7 @@ final class ChannelListPanelComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift index 918afe822c7..2c8c18860a9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift @@ -286,7 +286,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline), + canViewReactionList: canViewMessageReactionList(message: item.topMessage), animationCache: item.controllerInteraction.presentationContext.animationCache, animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift index bedbbf5c46c..dbe1880c410 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift @@ -15,6 +15,7 @@ import ChatMessageBubbleContentNode import ChatMessageItemCommon import ChatMessageInteractiveMediaNode import ChatControllerInteraction +import InvisibleInkDustNode public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { override public var supportsMosaic: Bool { @@ -26,6 +27,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { private var highlightedState: Bool = false private var media: Media? + private var mediaIndex: Int? private var automaticPlayback: Bool? override public var visibility: ListViewItemNodeVisibility { @@ -54,7 +56,8 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { case .automaticPlayback: openChatMessageMode = .automaticPlayback } - let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: openChatMessageMode)) + + let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: openChatMessageMode, mediaIndex: self.mediaIndex, progress: self.itemNode?.makeProgress())) } self.interactiveImageNode.activateAgeRestrictedMedia = { [weak self] in @@ -96,6 +99,8 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { return { item, layoutConstants, preparePosition, selection, constrainedSize, _ in var selectedMedia: Media? + var selectedMediaIndex: Int? + var extendedMedia: TelegramExtendedMedia? var automaticDownload: InteractiveMediaNodeAutodownloadMode = .none var automaticPlayback: Bool = false var contentMode: InteractiveMediaNodeContentMode = .aspectFit @@ -168,38 +173,48 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { contentMode = .aspectFill } else if let invoice = media as? TelegramMediaInvoice { selectedMedia = invoice + extendedMedia = invoice.extendedMedia + } + else if let paidContent = media as? TelegramMediaPaidContent { + selectedMedia = paidContent + if case let .mosaic(_, _, index) = preparePosition, let index { + extendedMedia = paidContent.extendedMedia[index] + selectedMediaIndex = index + } else { + extendedMedia = paidContent.extendedMedia.first + } + } + } + } - if let extendedMedia = invoice.extendedMedia, case let .full(media) = extendedMedia { - if let telegramImage = media as? TelegramMediaImage { - if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramImage) { - automaticDownload = .full - } - } else if let telegramFile = media as? TelegramMediaFile { - if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramFile) { - automaticDownload = .full - } else if shouldPredownloadMedia(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, media: telegramFile) { - automaticDownload = .prefetch - } - - if !item.message.containsSecretMedia { - if telegramFile.isAnimated && item.context.sharedContext.energyUsageSettings.autoplayGif { - if case .full = automaticDownload { - automaticPlayback = true - } else { - automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil - } - } else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo { - if case .full = automaticDownload { - automaticPlayback = true - } else { - automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil - } - } - } - contentMode = .aspectFill + if let extendedMedia, case let .full(media) = extendedMedia { + if let telegramImage = media as? TelegramMediaImage { + if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramImage) { + automaticDownload = .full + } + } else if let telegramFile = media as? TelegramMediaFile { + if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramFile) { + automaticDownload = .full + } else if shouldPredownloadMedia(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, media: telegramFile) { + automaticDownload = .prefetch + } + + if !item.message.containsSecretMedia { + if telegramFile.isAnimated && item.context.sharedContext.energyUsageSettings.autoplayGif { + if case .full = automaticDownload { + automaticPlayback = true + } else { + automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil + } + } else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo { + if case .full = automaticDownload { + automaticPlayback = true + } else { + automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil } } } + contentMode = .aspectFill } } @@ -341,7 +356,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { ) } - let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData, item.presentationData.dateTimeFormat, item.message, item.associatedData, item.attributes, selectedMedia!, dateAndStatus, automaticDownload, item.associatedData.automaticDownloadPeerType, item.associatedData.automaticDownloadPeerId, sizeCalculation, layoutConstants, contentMode, item.controllerInteraction.presentationContext) + let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData, item.presentationData.dateTimeFormat, item.message, item.associatedData, item.attributes, selectedMedia!, selectedMediaIndex, dateAndStatus, automaticDownload, item.associatedData.automaticDownloadPeerType, item.associatedData.automaticDownloadPeerId, sizeCalculation, layoutConstants, contentMode, item.controllerInteraction.presentationContext) let forceFullCorners = false let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 7.0, hidesBackground: .emptyWallpaper, forceFullCorners: forceFullCorners, forceAlignment: .none) @@ -377,6 +392,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { if let strongSelf = self { strongSelf.item = item strongSelf.media = selectedMedia + strongSelf.mediaIndex = selectedMediaIndex strongSelf.automaticPlayback = automaticPlayback let imageFrame = CGRect(origin: CGPoint(x: bubbleInsets.left, y: bubbleInsets.top), size: imageSize) @@ -432,6 +448,9 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { if let invoice = currentMedia as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { currentMedia = fullMedia } + if let paidContent = currentMedia as? TelegramMediaPaidContent, case let .full(fullMedia) = paidContent.extendedMedia[self.mediaIndex ?? 0] { + currentMedia = fullMedia + } if currentMedia.isSemanticallyEqual(to: media) { return self.interactiveImageNode.transitionNode(adjustRect: adjustRect) } @@ -446,7 +465,9 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { if let invoice = currentMedia as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { currentMedia = fullMedia } - + if let paidContent = currentMedia as? TelegramMediaPaidContent, case let .full(fullMedia) = paidContent.extendedMedia[self.mediaIndex ?? 0] { + currentMedia = fullMedia + } if let currentMedia = currentMedia, let media = media { for item in media { if item.isSemanticallyEqual(to: currentMedia) { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 4d49dca6b98..332531f153b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -1127,7 +1127,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline), + canViewReactionList: canViewMessageReactionList(message: item.topMessage), animationCache: item.controllerInteraction.presentationContext.animationCache, animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift index bc1f7ca0a17..96b9be4346e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift @@ -77,6 +77,10 @@ public final class MessageReactionButtonsNode: ASDisplayNode { selectedBackground: themeColors.reactionActiveBackground.argb, deselectedForeground: themeColors.reactionInactiveForeground.argb, selectedForeground: themeColors.reactionActiveForeground.argb, + deselectedStarsBackground: themeColors.reactionStarsInactiveBackground.argb, + selectedStarsBackground: themeColors.reactionStarsActiveBackground.argb, + deselectedStarsForeground: themeColors.reactionStarsInactiveForeground.argb, + selectedStarsForeground: themeColors.reactionStarsActiveForeground.argb, extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb, extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb, extractedSelectedForeground: presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb, @@ -90,6 +94,10 @@ public final class MessageReactionButtonsNode: ASDisplayNode { selectedBackground: themeColors.reactionActiveBackground.argb, deselectedForeground: themeColors.reactionInactiveForeground.argb, selectedForeground: themeColors.reactionActiveForeground.argb, + deselectedStarsBackground: themeColors.reactionStarsInactiveBackground.argb, + selectedStarsBackground: themeColors.reactionStarsActiveBackground.argb, + deselectedStarsForeground: themeColors.reactionStarsInactiveForeground.argb, + selectedStarsForeground: themeColors.reactionStarsActiveForeground.argb, extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb, extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb, extractedSelectedForeground: presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb, @@ -108,6 +116,10 @@ public final class MessageReactionButtonsNode: ASDisplayNode { selectedBackground: themeColors.reactionActiveBackground.argb, deselectedForeground: themeColors.reactionInactiveForeground.argb, selectedForeground: themeColors.reactionActiveForeground.argb, + deselectedStarsBackground: selectReactionFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, isStars: true).argb, + selectedStarsBackground: themeColors.reactionStarsActiveBackground.argb, + deselectedStarsForeground: themeColors.reactionStarsInactiveForeground.argb, + selectedStarsForeground: themeColors.reactionStarsActiveForeground.argb, extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb, extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb, extractedSelectedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb, @@ -350,16 +362,13 @@ public final class MessageReactionButtonsNode: ASDisplayNode { let itemValue = item.value let itemNode = item.node item.node.view.isGestureEnabled = true - let canViewReactionList = canViewMessageReactionList(message: message, isInline: associatedData.isInline) + let canViewReactionList = canViewMessageReactionList(message: message) item.node.view.activateAfterCompletion = !canViewReactionList item.node.view.activated = { [weak itemNode] gesture, _ in guard let strongSelf = self, let itemNode = itemNode else { gesture.cancel() return } - if !canViewReactionList { - return - } strongSelf.openReactionPreview?(gesture, itemNode.view.containerView, itemValue) } item.node.view.additionalActivationProgressLayer = itemMaskView.layer diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift index 2b47aa6ee77..8d5e6332931 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -142,7 +142,7 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline), + canViewReactionList: canViewMessageReactionList(message: item.topMessage), animationCache: item.controllerInteraction.presentationContext.animationCache, animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStarsMediaInfoNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageStarsMediaInfoNode/BUILD new file mode 100644 index 00000000000..85588cbecc7 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStarsMediaInfoNode/BUILD @@ -0,0 +1,32 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageStarsMediaInfoNode", + module_name = "ChatMessageStarsMediaInfoNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Postbox", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/LocalizedPeerData", + "//submodules/PhotoResources", + "//submodules/TelegramStringFormatting", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/ComponentFlow", + "//submodules/WallpaperBackgroundNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStarsMediaInfoNode/Sources/ChatMessageStarsMediaInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStarsMediaInfoNode/Sources/ChatMessageStarsMediaInfoNode.swift new file mode 100644 index 00000000000..25836a51b20 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStarsMediaInfoNode/Sources/ChatMessageStarsMediaInfoNode.swift @@ -0,0 +1,292 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Postbox +import Display +import TelegramCore +import SwiftSignalKit +import TelegramPresentationData +import AccountContext +import LocalizedPeerData +import PhotoResources +import TelegramStringFormatting +import TextFormat +import TextNodeWithEntities +import AnimationCache +import MultiAnimationRenderer +import ComponentFlow +import ChatControllerInteraction + +private func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat) -> (CGPoint, UIImage?) { + enum CornerType { + case topLeft + case topRight + case bottomLeft + case bottomRight + } + + func drawFullCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) { + if radius.isZero { + return + } + context.setFillColor(color.cgColor) + switch type { + case .topLeft: + context.clear(CGRect(origin: point, size: CGSize(width: radius, height: radius))) + context.fillEllipse(in: CGRect(origin: point, size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .topRight: + context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .bottomLeft: + context.clear(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .bottomRight: + context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + } + } + + func drawConnectingCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) { + context.setFillColor(color.cgColor) + switch type { + case .topLeft: + context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .topRight: + context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius, height: radius))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .bottomLeft: + context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .bottomRight: + context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + } + } + + if rects.isEmpty { + return (CGPoint(), nil) + } + + var topLeft = rects[0].origin + var bottomRight = CGPoint(x: rects[0].maxX, y: rects[0].maxY) + for i in 1 ..< rects.count { + topLeft.x = min(topLeft.x, rects[i].origin.x) + topLeft.y = min(topLeft.y, rects[i].origin.y) + bottomRight.x = max(bottomRight.x, rects[i].maxX) + bottomRight.y = max(bottomRight.y, rects[i].maxY) + } + + topLeft.x -= inset + topLeft.y -= inset + bottomRight.x += inset * 2.0 + bottomRight.y += inset * 2.0 + + return (topLeft, generateImage(CGSize(width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + + context.setBlendMode(.copy) + + for i in 0 ..< rects.count { + let rect = rects[i].insetBy(dx: -inset, dy: -inset) + context.fill(rect.offsetBy(dx: -topLeft.x, dy: -topLeft.y)) + } + + for i in 0 ..< rects.count { + let rect = rects[i].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) + + var previous: CGRect? + if i != 0 { + previous = rects[i - 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) + } + + var next: CGRect? + if i != rects.count - 1 { + next = rects[i + 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) + } + + if let previous = previous { + if previous.contains(rect.topLeft) { + if abs(rect.topLeft.x - previous.minX) >= innerRadius { + var radius = innerRadius + if let next = next { + radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) + } + drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topLeft.x, y: previous.maxY), type: .topLeft, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius) + } + if previous.contains(rect.topRight.offsetBy(dx: -1.0, dy: 0.0)) { + if abs(rect.topRight.x - previous.maxX) >= innerRadius { + var radius = innerRadius + if let next = next { + radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) + } + drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topRight.x, y: previous.maxY), type: .topRight, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius) + drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius) + } + + if let next = next { + if next.contains(rect.bottomLeft) { + if abs(rect.bottomRight.x - next.maxX) >= innerRadius { + var radius = innerRadius + if let previous = previous { + radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) + } + drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomLeft.x, y: next.minY), type: .bottomLeft, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius) + } + if next.contains(rect.bottomRight.offsetBy(dx: -1.0, dy: 0.0)) { + if abs(rect.bottomRight.x - next.maxX) >= innerRadius { + var radius = innerRadius + if let previous = previous { + radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) + } + drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomRight.x, y: next.minY), type: .bottomRight, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius) + drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius) + } + } + })) +} + +public enum ChatMessageThreadInfoType { + case bubble(incoming: Bool) + case standalone +} + +public class ChatMessageStarsMediaInfoNode: ASDisplayNode { + public class Arguments { + public let presentationData: ChatPresentationData + public let context: AccountContext + public let message: Message + public let media: TelegramMediaPaidContent + public let constrainedSize: CGSize + public let animationCache: AnimationCache? + public let animationRenderer: MultiAnimationRenderer? + + public init( + presentationData: ChatPresentationData, + context: AccountContext, + message: Message, + media: TelegramMediaPaidContent, + constrainedSize: CGSize, + animationCache: AnimationCache?, + animationRenderer: MultiAnimationRenderer? + ) { + self.presentationData = presentationData + self.context = context + self.message = message + self.media = media + self.constrainedSize = constrainedSize + self.animationCache = animationCache + self.animationRenderer = animationRenderer + } + } + + public var visibility: Bool = false { + didSet { + if self.visibility != oldValue { + self.textNode?.visibilityRect = self.visibility ? CGRect.infinite : nil + } + } + } + + private let contentBackgroundNode: ASImageNode + private var textNode: TextNodeWithEntities? + + override public init() { + self.contentBackgroundNode = ASImageNode() + self.contentBackgroundNode.displaysAsynchronously = false + self.contentBackgroundNode.displayWithoutProcessing = true + self.contentBackgroundNode.isLayerBacked = true + self.contentBackgroundNode.isUserInteractionEnabled = false + + super.init() + + self.addSubnode(self.contentBackgroundNode) + } + + public class func asyncLayout(_ maybeNode: ChatMessageStarsMediaInfoNode?) -> (_ arguments: Arguments) -> (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode) { + let textNodeLayout = TextNodeWithEntities.asyncLayout(maybeNode?.textNode) + + return { arguments in + let fontSize = floor(arguments.presentationData.fontSize.baseDisplaySize * 11.0 / 17.0) + let textFont = Font.regular(fontSize) + + let text: NSMutableAttributedString + if let peer = arguments.message.peers[arguments.message.id.peerId] as? TelegramChannel, peer.flags.contains(.isCreator) || peer.adminRights != nil, arguments.message.forwardInfo == nil { + let amountString = presentationStringsFormattedNumber(Int32(arguments.media.amount), arguments.presentationData.dateTimeFormat.groupingSeparator) + text = NSMutableAttributedString(string: "⭐️\(amountString)", font: textFont, textColor: .white) + } else { + text = NSMutableAttributedString(string: arguments.presentationData.strings.Chat_PaidMedia_Purchased, font: textFont, textColor: .white) + } + + var offset: CGFloat = 0.0 + if let range = text.string.range(of: "⭐️") { + text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: text.string)) + text.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: text.string)) + offset -= 1.0 + } + + let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: arguments.constrainedSize.width, height: arguments.constrainedSize.height), alignment: .natural, cutout: nil, insets: .zero)) + + let padding: CGFloat = 6.0 + let size = CGSize(width: textLayout.size.width + padding * 2.0, height: 18.0) + + return (size, { attemptSynchronous in + let node: ChatMessageStarsMediaInfoNode + if let maybeNode = maybeNode { + node = maybeNode + } else { + node = ChatMessageStarsMediaInfoNode() + } + + if node.contentBackgroundNode.image == nil { + node.contentBackgroundNode.image = generateStretchableFilledCircleImage(radius: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3)) + } + + node.textNode?.textNode.displaysAsynchronously = !arguments.presentationData.isPreview + + var textArguments: TextNodeWithEntities.Arguments? + if let cache = arguments.animationCache, let renderer = arguments.animationRenderer { + textArguments = TextNodeWithEntities.Arguments(context: arguments.context, cache: cache, renderer: renderer, placeholderColor: .clear, attemptSynchronous: attemptSynchronous) + } + let textNode = textApply(textArguments) + textNode.visibilityRect = node.visibility ? CGRect.infinite : nil + + if node.textNode == nil { + textNode.textNode.isUserInteractionEnabled = false + node.textNode = textNode + node.addSubnode(textNode.textNode) + } + + node.contentBackgroundNode.frame = CGRect(origin: .zero, size: size) + + let textFrame = CGRect(origin: CGPoint(x: padding + offset, y: floorToScreenPixels((size.height - textLayout.size.height) / 2.0) + UIScreenPixel), size: textLayout.size) + textNode.textNode.frame = textFrame + + return node + }) + } + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index 9962ab06882..31cc3353a60 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -647,7 +647,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.message, isInline: item.associatedData.isInline), + canViewReactionList: canViewMessageReactionList(message: item.message), animationCache: item.controllerInteraction.presentationContext.animationCache, animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index ee47fa2bc28..2055bbcf126 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -111,7 +111,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private var expandedBlockIds: Set = Set() private var appliedExpandedBlockIds: Set? - private var displayContentsUnderSpoilers: Bool = false + private var displayContentsUnderSpoilers: (value: Bool, location: CGPoint?) = (false, nil) override public var visibility: ListViewItemNodeVisibility { didSet { @@ -162,11 +162,18 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } item.controllerInteraction.requestMessageUpdate(item.message.id, false) } - self.textNode.textNode.requestDisplayContentsUnderSpoilers = { [weak self] in + self.textNode.textNode.requestDisplayContentsUnderSpoilers = { [weak self] location in guard let self else { return } - self.updateDisplayContentsUnderSpoilers(value: true) + + cancelParentGestures(view: self.view) + + var mappedLocation: CGPoint? + if let location { + mappedLocation = self.textNode.textNode.layer.convert(location, to: self.layer) + } + self.updateDisplayContentsUnderSpoilers(value: true, at: mappedLocation) } self.textNode.textNode.canHandleTapAtPoint = { [weak self] point in guard let self else { @@ -231,11 +238,13 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let message = item.message - let incoming: Bool - if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { - incoming = false - } else { - incoming = item.message.effectivelyIncoming(item.context.account.peerId) + var incoming = item.message.effectivelyIncoming(item.context.account.peerId) + if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject { + if case .forward = info { + incoming = false + } else if case let .link(link) = info, link.isCentered { + incoming = true + } } var maxTextWidth = CGFloat.greatestFiniteMagnitude @@ -300,6 +309,10 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } else { displayStatus = false } + } else if !item.presentationData.chatBubbleCorners.hasTails { + displayStatus = false + } else if case let .messageOptions(_, _, info) = item.associatedData.subject, case let .link(link) = info, link.isCentered { + displayStatus = false } if displayStatus { if incoming { @@ -546,7 +559,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { attributedText = updatedString } - var customTruncationToken: NSAttributedString? + var customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)? var maximumNumberOfLines: Int = 0 if item.presentationData.isPreview { if item.message.groupingKey != nil { @@ -569,10 +582,17 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { maximumNumberOfLines = 12 } - let truncationToken = NSMutableAttributedString() - truncationToken.append(NSAttributedString(string: "\u{2026} ", font: textFont, textColor: messageTheme.primaryTextColor)) - truncationToken.append(NSAttributedString(string: item.presentationData.strings.Conversation_ReadMore, font: textFont, textColor: messageTheme.accentTextColor)) - customTruncationToken = truncationToken + let truncationTokenText = item.presentationData.strings.Conversation_ReadMore + customTruncationToken = { baseFont, isQuote in + let truncationToken = NSMutableAttributedString() + if isQuote { + truncationToken.append(NSAttributedString(string: "\u{2026}", font: Font.regular(baseFont.pointSize), textColor: messageTheme.primaryTextColor)) + } else { + truncationToken.append(NSAttributedString(string: "\u{2026} ", font: Font.regular(baseFont.pointSize), textColor: messageTheme.primaryTextColor)) + truncationToken.append(NSAttributedString(string: truncationTokenText, font: Font.regular(baseFont.pointSize), textColor: messageTheme.accentTextColor)) + } + return truncationToken + } } let textInsets = UIEdgeInsets(top: 2.0, left: 2.0, bottom: 5.0, right: 2.0) @@ -586,7 +606,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor, - displayContentsUnderSpoilers: displayContentsUnderSpoilers, + displayContentsUnderSpoilers: displayContentsUnderSpoilers.value, customTruncationToken: customTruncationToken, expandedBlocks: expandedBlockIds )) @@ -629,7 +649,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && (!item.associatedData.isInPinnedListMode || isReplyThread), hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline), + canViewReactionList: canViewMessageReactionList(message: item.topMessage), animationCache: item.controllerInteraction.presentationContext.animationCache, animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) @@ -677,6 +697,26 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } strongSelf.appliedExpandedBlockIds = strongSelf.expandedBlockIds + var spoilerExpandRect: CGRect? + if let location = strongSelf.displayContentsUnderSpoilers.location { + strongSelf.displayContentsUnderSpoilers.location = nil + + let mappedLocation = CGPoint(x: location.x - textFrame.minX, y: location.y - textFrame.minY) + + let getDistance: (CGPoint, CGPoint) -> CGFloat = { a, b in + let v = CGPoint(x: a.x - b.x, y: a.y - b.y) + return sqrt(v.x * v.x + v.y * v.y) + } + + var maxDistance: CGFloat = getDistance(mappedLocation, CGPoint(x: 0.0, y: 0.0)) + maxDistance = max(maxDistance, getDistance(mappedLocation, CGPoint(x: textFrame.width, y: 0.0))) + maxDistance = max(maxDistance, getDistance(mappedLocation, CGPoint(x: textFrame.width, y: textFrame.height))) + maxDistance = max(maxDistance, getDistance(mappedLocation, CGPoint(x: 0.0, y: textFrame.height))) + + let mappedSize = CGSize(width: maxDistance * 2.0, height: maxDistance * 2.0) + spoilerExpandRect = mappedSize.centered(around: mappedLocation) + } + let _ = textApply(InteractiveTextNodeWithEntities.Arguments( context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, @@ -685,7 +725,13 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { attemptSynchronous: synchronousLoads, textColor: messageTheme.primaryTextColor, spoilerEffectColor: messageTheme.secondaryTextColor, - animation: animation + applyArguments: InteractiveTextNode.ApplyArguments( + animation: animation, + spoilerTextColor: messageTheme.primaryTextColor, + spoilerEffectColor: messageTheme.secondaryTextColor, + areContentAnimationsEnabled: item.context.sharedContext.energyUsageSettings.loopEmoji, + spoilerExpandRect: spoilerExpandRect + ) )) animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: textFrame, completion: nil) @@ -850,10 +896,58 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } + func makeActivate(_ urlRange: NSRange?) -> (() -> Promise?)? { + return { [weak self] in + guard let self else { + return nil + } + + let promise = Promise() + + self.linkProgressDisposable?.dispose() + + if self.linkProgressRange != nil { + self.linkProgressRange = nil + self.updateLinkProgressState() + } + + self.linkProgressDisposable = (promise.get() |> deliverOnMainQueue).startStrict(next: { [weak self] value in + guard let self else { + return + } + let updatedRange: NSRange? = value ? urlRange : nil + if self.linkProgressRange != updatedRange { + self.linkProgressRange = updatedRange + self.updateLinkProgressState() + } + }) + + return promise + } + } + let textNodeFrame = self.textNode.textNode.frame let textLocalPoint = CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY) if let (index, attributes) = self.textNode.textNode.attributesAtPoint(textLocalPoint) { - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !self.displayContentsUnderSpoilers { + var rects: [CGRect]? + let possibleNames: [String] = [ + TelegramTextAttributes.URL, + TelegramTextAttributes.PeerMention, + TelegramTextAttributes.PeerTextMention, + TelegramTextAttributes.BotCommand, + TelegramTextAttributes.Hashtag, + TelegramTextAttributes.Timecode, + TelegramTextAttributes.BankCard + ] + for name in possibleNames { + if let _ = attributes[NSAttributedString.Key(rawValue: name)] { + rects = self.textNode.textNode.attributeRects(name: name, at: index) + break + } + } + + + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !self.displayContentsUnderSpoilers.value { return ChatMessageBubbleContentTapAction(content: .none) } else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { var concealed = true @@ -870,76 +964,28 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { content = .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)) } - return ChatMessageBubbleContentTapAction(content: content, activate: { [weak self] in - guard let self else { - return nil - } - - let promise = Promise() - - self.linkProgressDisposable?.dispose() - - if self.linkProgressRange != nil { - self.linkProgressRange = nil - self.updateLinkProgressState() - } - - self.linkProgressDisposable = (promise.get() |> deliverOnMainQueue).startStrict(next: { [weak self] value in - guard let self else { - return - } - let updatedRange: NSRange? = value ? urlRange : nil - if self.linkProgressRange != updatedRange { - self.linkProgressRange = updatedRange - self.updateLinkProgressState() - } - }) - - return promise - }) + return ChatMessageBubbleContentTapAction(content: content, rects: rects, activate: makeActivate(urlRange)) } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { - return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)) + return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false), rects: rects) } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { var urlRange: NSRange? if let (_, _, urlRangeValue) = self.textNode.textNode.attributeSubstringWithRange(name: TelegramTextAttributes.PeerTextMention, index: index) { urlRange = urlRangeValue } - return ChatMessageBubbleContentTapAction(content: .textMention(peerName), activate: { [weak self] in - guard let self else { - return nil - } - - let promise = Promise() - - self.linkProgressDisposable?.dispose() - - if self.linkProgressRange != nil { - self.linkProgressRange = nil - self.updateLinkProgressState() - } - - self.linkProgressDisposable = (promise.get() |> deliverOnMainQueue).startStrict(next: { [weak self] value in - guard let self else { - return - } - let updatedRange: NSRange? = value ? urlRange : nil - if self.linkProgressRange != updatedRange { - self.linkProgressRange = updatedRange - self.updateLinkProgressState() - } - }) - - return promise - }) + return ChatMessageBubbleContentTapAction(content: .textMention(peerName), rects: rects, activate: makeActivate(urlRange)) } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { - return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand)) + return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand), rects: rects) } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { - return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag)) + return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag), rects: rects) } else if let timecode = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Timecode)] as? TelegramTimecode { - return ChatMessageBubbleContentTapAction(content: .timecode(timecode.time, timecode.text)) + return ChatMessageBubbleContentTapAction(content: .timecode(timecode.time, timecode.text), rects: rects) } else if let bankCard = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard)] as? String { - return ChatMessageBubbleContentTapAction(content: .bankCard(bankCard)) + var urlRange: NSRange? + if let (_, _, urlRangeValue) = self.textNode.textNode.attributeSubstringWithRange(name: TelegramTextAttributes.BankCard, index: index) { + urlRange = urlRangeValue + } + return ChatMessageBubbleContentTapAction(content: .bankCard(bankCard), rects: rects, activate: makeActivate(urlRange)) } else if let pre = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre)] as? String { return ChatMessageBubbleContentTapAction(content: .copy(pre)) } else if let code = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Code)] as? String { @@ -1045,7 +1091,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - if let spoilerRects = spoilerRects, !spoilerRects.isEmpty, !self.displayContentsUnderSpoilers { + if let spoilerRects = spoilerRects, !spoilerRects.isEmpty, !self.displayContentsUnderSpoilers.value { } else if let rects = rects { let linkHighlightingNode: LinkHighlightingNode if let current = self.linkHighlightingNode { @@ -1315,11 +1361,11 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { guard let strongSelf = self else { return } - if !strongSelf.displayContentsUnderSpoilers, let textLayout = strongSelf.textNode.textNode.cachedLayout, textLayout.segments.contains(where: { !$0.spoilers.isEmpty }), let selectionRange { + if !strongSelf.displayContentsUnderSpoilers.value, let textLayout = strongSelf.textNode.textNode.cachedLayout, textLayout.segments.contains(where: { !$0.spoilers.isEmpty }), let selectionRange { for segment in textLayout.segments { for (spoilerRange, _) in segment.spoilers { if let intersection = selectionRange.intersection(spoilerRange), intersection.length > 0 { - strongSelf.updateDisplayContentsUnderSpoilers(value: true) + strongSelf.updateDisplayContentsUnderSpoilers(value: true, at: nil) return } } @@ -1375,17 +1421,17 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { }) } - if self.displayContentsUnderSpoilers { - self.updateDisplayContentsUnderSpoilers(value: false) + if self.displayContentsUnderSpoilers.value { + self.updateDisplayContentsUnderSpoilers(value: false, at: nil) } } } - private func updateDisplayContentsUnderSpoilers(value: Bool) { - if self.displayContentsUnderSpoilers == value { + private func updateDisplayContentsUnderSpoilers(value: Bool, at location: CGPoint?) { + if self.displayContentsUnderSpoilers.value == value { return } - self.displayContentsUnderSpoilers = value + self.displayContentsUnderSpoilers = (value, location) if let item = self.item { item.controllerInteraction.requestMessageUpdate(item.message.id, false) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode/Sources/ChatMessageTransitionNode.swift index 762d64392b3..b8a5565050f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode/Sources/ChatMessageTransitionNode.swift @@ -10,6 +10,6 @@ public protocol ChatMessageTransitionNodeDecorationItemNode: ASDisplayNode { public protocol ChatMessageTransitionNode: AnyObject { typealias DecorationItemNode = ChatMessageTransitionNodeDecorationItemNode - func add(decorationView: UIView, itemNode: ChatMessageItemNodeProtocol) -> DecorationItemNode + func add(decorationView: UIView, itemNode: ChatMessageItemNodeProtocol, aboveEverything: Bool) -> DecorationItemNode func remove(decorationNode: DecorationItemNode) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageUnlockMediaNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageUnlockMediaNode/BUILD new file mode 100644 index 00000000000..a031cda0e19 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageUnlockMediaNode/BUILD @@ -0,0 +1,37 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageUnlockMediaNode", + module_name = "ChatMessageUnlockMediaNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Postbox", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/LocalizedPeerData", + "//submodules/PhotoResources", + "//submodules/TelegramStringFormatting", + "//submodules/TextFormat", + "//submodules/InvisibleInkDustNode", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/WallpaperBackgroundNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/AvatarNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageUnlockMediaNode/Sources/ChatMessageUnlockMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageUnlockMediaNode/Sources/ChatMessageUnlockMediaNode.swift new file mode 100644 index 00000000000..91b51a70a9c --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageUnlockMediaNode/Sources/ChatMessageUnlockMediaNode.swift @@ -0,0 +1,345 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Postbox +import Display +import TelegramCore +import SwiftSignalKit +import TelegramPresentationData +import AccountContext +import LocalizedPeerData +import PhotoResources +import TelegramStringFormatting +import TextFormat +import TextNodeWithEntities +import AnimationCache +import MultiAnimationRenderer +import ComponentFlow +import ChatControllerInteraction +import HierarchyTrackingLayer + +public class ChatMessageUnlockMediaNode: ASDisplayNode { + public class Arguments { + public let presentationData: ChatPresentationData + public let strings: PresentationStrings + public let context: AccountContext + public let controllerInteraction: ChatControllerInteraction + public let message: Message + public let media: TelegramMediaPaidContent + public let constrainedSize: CGSize + public let animationCache: AnimationCache? + public let animationRenderer: MultiAnimationRenderer? + + public init( + presentationData: ChatPresentationData, + strings: PresentationStrings, + context: AccountContext, + controllerInteraction: ChatControllerInteraction, + message: Message, + media: TelegramMediaPaidContent, + constrainedSize: CGSize, + animationCache: AnimationCache?, + animationRenderer: MultiAnimationRenderer? + ) { + self.presentationData = presentationData + self.strings = strings + self.context = context + self.controllerInteraction = controllerInteraction + self.message = message + self.media = media + self.constrainedSize = constrainedSize + self.animationCache = animationCache + self.animationRenderer = animationRenderer + } + } + + public var visibility: Bool = false { + didSet { + if self.visibility != oldValue { + self.textNode?.visibilityRect = self.visibility ? CGRect.infinite : nil + } + } + } + + private let contentNode: HighlightTrackingButtonNode + private let backgroundNode: NavigationBackgroundNode + private var textNode: TextNodeWithEntities? + private var loadingView: LoadingEffectView? + + private var pressed = { } + + private var absolutePosition: (CGRect, CGSize)? + + private var currentProgressDisposable: Disposable? + + override public init() { + self.contentNode = HighlightTrackingButtonNode() + + self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x000000, alpha: 0.3)) + + super.init() + + self.contentNode.isUserInteractionEnabled = true + + self.addSubnode(self.contentNode) + self.contentNode.addSubnode(self.backgroundNode) + + self.contentNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + deinit { + self.currentProgressDisposable?.dispose() + } + + @objc private func buttonPressed() { + self.pressed() + } + + public func makeProgress() -> Promise { + let progress = Promise() + self.currentProgressDisposable?.dispose() + self.currentProgressDisposable = (progress.get() + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] hasProgress in + guard let self, let loadingView = self.loadingView else { + return + } + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + if hasProgress { + if loadingView.superview == nil { + loadingView.alpha = 0.0 + self.view.addSubview(loadingView) + transition.updateAlpha(layer: loadingView.layer, alpha: 1.0) + } + } else if loadingView.superview != nil { + transition.updateAlpha(layer: loadingView.layer, alpha: 0.0, beginWithCurrentState: true, completion: { finished in + if finished { + loadingView.removeFromSuperview() + } + }) + } + }) + return progress + } + + public class func asyncLayout(_ maybeNode: ChatMessageUnlockMediaNode?) -> (_ arguments: Arguments) -> (CGSize, (Bool) -> ChatMessageUnlockMediaNode) { + let textNodeLayout = TextNodeWithEntities.asyncLayout(maybeNode?.textNode) + + return { arguments in + let fontSize = floor(arguments.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0) + let textFont = Font.medium(fontSize) + + let padding: CGFloat = 10.0 + let amountString = presentationStringsFormattedNumber(Int32(arguments.media.amount), arguments.presentationData.dateTimeFormat.groupingSeparator) + let text = NSMutableAttributedString(string: arguments.presentationData.strings.Chat_PaidMedia_UnlockMedia("⭐️ \(amountString)").string, font: textFont, textColor: .white) + if let range = text.string.range(of: "⭐️") { + text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: text.string)) + text.addAttribute(.baselineOffset, value: 0.5, range: NSRange(range, in: text.string)) + } + + let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: arguments.constrainedSize.width, height: arguments.constrainedSize.height), alignment: .natural, cutout: nil, insets: .zero)) + + let size = CGSize(width: textLayout.size.width + padding * 2.0, height: 32.0) + + return (size, { attemptSynchronous in + let node: ChatMessageUnlockMediaNode + if let maybeNode = maybeNode { + node = maybeNode + } else { + node = ChatMessageUnlockMediaNode() + } + + node.pressed = { + let _ = arguments.controllerInteraction.openMessage(arguments.message, OpenMessageParams(mode: .default)) + } + + node.textNode?.textNode.displaysAsynchronously = !arguments.presentationData.isPreview + + var textArguments: TextNodeWithEntities.Arguments? + if let cache = arguments.animationCache, let renderer = arguments.animationRenderer { + textArguments = TextNodeWithEntities.Arguments(context: arguments.context, cache: cache, renderer: renderer, placeholderColor: .clear, attemptSynchronous: attemptSynchronous) + } + let textNode = textApply(textArguments) + textNode.visibilityRect = node.visibility ? CGRect.infinite : nil + + if node.textNode == nil { + textNode.textNode.isUserInteractionEnabled = false + node.textNode = textNode + node.contentNode.addSubnode(textNode.textNode) + } + + let textFrame = CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((size.height - textLayout.size.height) / 2.0)), size: textLayout.size) + textNode.textNode.frame = textFrame + + node.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) + node.backgroundNode.update(size: size, cornerRadius: size.height / 2.0, transition: .immediate) + node.contentNode.frame = CGRect(origin: CGPoint(), size: size) + + let loadingView: LoadingEffectView + if let current = node.loadingView { + loadingView = current + } else { + loadingView = LoadingEffectView() + node.loadingView = loadingView + } + loadingView.frame = CGRect(origin: .zero, size: size) + loadingView.update(color: UIColor.white, rect: CGRect(origin: .zero, size: size)) + + return node + }) + } + } +} + +private let shadowImage: UIImage? = { + UIImage(named: "Stories/PanelGradient") +}() + +public final class LoadingEffectView: UIView { + let hierarchyTrackingLayer: HierarchyTrackingLayer + + private let maskContentsView: UIView + private let maskHighlightNode: LinkHighlightingNode + + private let maskBorderContentsView: UIView + private let maskBorderHighlightNode: LinkHighlightingNode + + private let backgroundView: UIImageView + private let borderBackgroundView: UIImageView + + private var duration: Double + private var gradientWidth: CGFloat + + private var inHierarchy = false + private var size: CGSize? + + override public init(frame: CGRect) { + self.hierarchyTrackingLayer = HierarchyTrackingLayer() + + self.maskContentsView = UIView() + self.maskHighlightNode = LinkHighlightingNode(color: .black) + self.maskHighlightNode.useModernPathCalculation = true + + self.maskBorderContentsView = UIView() + self.maskBorderHighlightNode = LinkHighlightingNode(color: .black) + self.maskBorderHighlightNode.borderOnly = true + self.maskBorderHighlightNode.useModernPathCalculation = true + + self.maskBorderContentsView.addSubview(self.maskBorderHighlightNode.view) + + self.backgroundView = UIImageView() + self.borderBackgroundView = UIImageView() + + self.gradientWidth = 120.0 + self.duration = 1.0 + + super.init(frame: frame) + + self.isUserInteractionEnabled = false + + self.maskContentsView.mask = self.maskHighlightNode.view + self.maskContentsView.addSubview(self.backgroundView) + self.addSubview(self.maskContentsView) + + self.maskBorderContentsView.mask = self.maskBorderHighlightNode.view + self.maskBorderContentsView.addSubview(self.borderBackgroundView) + self.addSubview(self.maskBorderContentsView) + + let generateGradient: (CGFloat) -> UIImage? = { baseAlpha in + return generateImage(CGSize(width: self.gradientWidth, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let foregroundColor = UIColor(white: 1.0, alpha: min(1.0, baseAlpha * 4.0)) + + if let shadowImage { + UIGraphicsPushContext(context) + + for i in 0 ..< 2 { + let shadowFrame = CGRect(origin: CGPoint(x: CGFloat(i) * (size.width * 0.5), y: 0.0), size: CGSize(width: size.width * 0.5, height: size.height)) + + context.saveGState() + context.translateBy(x: shadowFrame.midX, y: shadowFrame.midY) + context.rotate(by: CGFloat(i == 0 ? 1.0 : -1.0) * CGFloat.pi * 0.5) + let adjustedRect = CGRect(origin: CGPoint(x: -shadowFrame.height * 0.5, y: -shadowFrame.width * 0.5), size: CGSize(width: shadowFrame.height, height: shadowFrame.width)) + + context.clip(to: adjustedRect, mask: shadowImage.cgImage!) + context.setFillColor(foregroundColor.cgColor) + context.fill(adjustedRect) + + context.restoreGState() + } + + UIGraphicsPopContext() + } + })?.withRenderingMode(.alwaysTemplate) + } + + self.backgroundView.image = generateGradient(0.5) + self.borderBackgroundView.image = generateGradient(1.0) + + self.layer.addSublayer(self.hierarchyTrackingLayer) + self.hierarchyTrackingLayer.isInHierarchyUpdated = { [weak self] inHierarchy in + guard let self, let size = self.size else { + return + } + self.inHierarchy = inHierarchy + self.updateAnimations(size: size) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateAnimations(size: CGSize) { + if self.inHierarchy { + if self.backgroundView.layer.animation(forKey: "shimmer") != nil { + return + } + let animation = self.backgroundView.layer.makeAnimation(from: 0.0 as NSNumber, to: (size.width + self.gradientWidth + size.width * 0.0) as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: self.duration, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = Float.infinity + self.backgroundView.layer.add(animation, forKey: "shimmer") + self.borderBackgroundView.layer.add(animation, forKey: "shimmer") + } else { + self.backgroundView.layer.removeAllAnimations() + self.borderBackgroundView.layer.removeAllAnimations() + } + } + + public func update(color: UIColor, rect: CGRect) { + let maskFrame = CGRect(origin: CGPoint(), size: rect.size).insetBy(dx: -4.0, dy: -4.0) + + self.gradientWidth = 260.0 + self.duration = 1.2 + + self.maskContentsView.backgroundColor = .clear + self.maskBorderContentsView.backgroundColor = color.withAlphaComponent(0.12) + +// self.backgroundView.alpha = 0.25 + self.backgroundView.tintColor = color + + self.borderBackgroundView.tintColor = color + + self.maskContentsView.frame = maskFrame + self.maskBorderContentsView.frame = maskFrame + + let rectsSet: [CGRect] = [rect] + + self.maskHighlightNode.outerRadius = rect.height / 2.0 + self.maskHighlightNode.updateRects(rectsSet) + self.maskHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize()) + + self.maskBorderHighlightNode.outerRadius = rect.height / 2.0 + self.maskBorderHighlightNode.updateRects(rectsSet) + self.maskBorderHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize()) + + if self.size != maskFrame.size { + self.size = maskFrame.size + + self.backgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height)) + self.borderBackgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height)) + + self.updateAnimations(size: maskFrame.size) + } + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index 0498b223cc3..9d73bee8ebb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -22,31 +22,6 @@ import ChatControllerInteraction private let titleFont: UIFont = Font.semibold(15.0) -public func defaultWebpageImageSizeIsSmall(webpage: TelegramMediaWebpageLoadedContent) -> Bool { - let type = websiteType(of: webpage.websiteName) - - let mainMedia: Media? - switch type { - case .instagram, .twitter: - mainMedia = webpage.story ?? webpage.image ?? webpage.file - default: - mainMedia = webpage.story ?? webpage.file ?? webpage.image - } - - if let image = mainMedia as? TelegramMediaImage { - if let type = webpage.type, (["photo", "video", "embed", "gif", "document", "telegram_album"] as [String]).contains(type) { - } else if let type = webpage.type, (["article"] as [String]).contains(type) { - return true - } else if let _ = largestImageRepresentation(image.representations)?.dimensions { - if webpage.instantPage == nil { - return true - } - } - } - - return false -} - public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { private var webPage: TelegramMediaWebpage? diff --git a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift index 629a1bcea3e..031d1724267 100644 --- a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift @@ -41,7 +41,7 @@ final class BlurredRoundedRectangle: Component { preconditionFailure() } - func update(component: BlurredRoundedRectangle, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: BlurredRoundedRectangle, availableSize: CGSize, transition: ComponentTransition) -> CGSize { transition.setFrame(view: self.background.view, frame: CGRect(origin: CGPoint(), size: availableSize)) self.background.updateColor(color: component.color, transition: .immediate) self.background.update(size: availableSize, cornerRadius: min(availableSize.width, availableSize.height) / 2.0, transition: .immediate) @@ -54,7 +54,7 @@ final class BlurredRoundedRectangle: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -96,7 +96,7 @@ final class RadialProgressComponent: Component { preconditionFailure() } - func update(component: RadialProgressComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: RadialProgressComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { func draw(context: CGContext) { let diameter = availableSize.width @@ -168,7 +168,7 @@ final class RadialProgressComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -277,7 +277,7 @@ final class CheckComponent: Component { } } - func update(component: CheckComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: CheckComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { if let currentValue = self.currentValue, currentValue != component.value, case .curve = transition.animation { self.animator?.invalidate() @@ -310,7 +310,7 @@ final class CheckComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -513,7 +513,7 @@ final class AvatarComponent: Component { preconditionFailure() } - func update(component: AvatarComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: AvatarComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.avatarContainer.frame = CGRect(origin: CGPoint(), size: availableSize) let theme = component.context.sharedContext.currentPresentationData.with({ $0 }).theme @@ -643,7 +643,7 @@ final class AvatarComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -729,7 +729,7 @@ private final class WallpaperBlurComponent: Component { preconditionFailure() } - func update(component: WallpaperBlurComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: WallpaperBlurComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { transition.setFrame(view: self.background.view, frame: CGRect(origin: CGPoint(), size: availableSize)) self.background.update(rect: component.rect, within: component.withinSize, color: component.color, wallpaperNode: component.wallpaperNode, transition: .immediate) @@ -741,7 +741,7 @@ private final class WallpaperBlurComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -900,7 +900,7 @@ final class OverscrollContentsComponent: Component { preconditionFailure() } - func update(component: OverscrollContentsComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: OverscrollContentsComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { if let _ = component.peer { self.avatarView.isHidden = false self.checkView.isHidden = true @@ -1034,7 +1034,7 @@ final class OverscrollContentsComponent: Component { let checkSize: CGFloat = 56.0 self.checkView.frame = CGRect(origin: CGPoint(x: floor(-checkSize / 2.0), y: floor(-checkSize / 2.0)), size: CGSize(width: checkSize, height: checkSize)) let _ = self.checkView.update( - transition: Transition(animation: transformTransition.isAnimated ? .curve(duration: 0.2, curve: .easeInOut) : .none), + transition: ComponentTransition(animation: transformTransition.isAnimated ? .curve(duration: 0.2, curve: .easeInOut) : .none), component: AnyComponent(CheckComponent( color: component.foregroundColor, lineWidth: 3.0, @@ -1046,7 +1046,7 @@ final class OverscrollContentsComponent: Component { if let peer = component.peer { let _ = self.avatarView.update( - transition: Transition(animation: transformTransition.isAnimated ? .curve(duration: 0.2, curve: .easeInOut) : .none), + transition: ComponentTransition(animation: transformTransition.isAnimated ? .curve(duration: 0.2, curve: .easeInOut) : .none), component: AnyComponent(AvatarComponent( context: component.context, peer: peer, @@ -1081,7 +1081,7 @@ final class OverscrollContentsComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 73f547d8392..7bdc5a1bdec 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -230,7 +230,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break } } - let gallerySource = GalleryControllerItemSource.standaloneMessage(message) + let gallerySource = GalleryControllerItemSource.standaloneMessage(message, nil) return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: nil, chatFilterTag: nil, chatLocationContextHolder: nil, message: message, standalone: true, reverseMessageGalleryOrder: false, navigationController: navigationController, dismissInput: { //self?.chatDisplayNode.dismissInput() }, present: { c, a in @@ -363,7 +363,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { return self?.getNavigationController() }, chatControllerNode: { [weak self] in return self - }, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { [weak self] action, message in + }, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { [weak self] action, params in if let strongSelf = self { switch action { case let .url(url): @@ -428,6 +428,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }) ])]) strongSelf.presentController(actionSheet, .window(.root), nil) + case let .phone(number): + let _ = number + break case let .peerMention(peerId, mention): let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] @@ -525,7 +528,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { ])]) strongSelf.presentController(actionSheet, .window(.root), nil) case let .timecode(timecode, text): - guard let message = message else { + guard let message = params?.message else { return } let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) @@ -554,7 +557,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break } } - }, openCheckoutOrReceipt: { _ in + }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in @@ -614,7 +617,6 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in }, openStickerEditor: { - }, openPhoneContextMenu: { _ in }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift index ab70fa9a0da..7126d2d6208 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift @@ -1278,7 +1278,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let mediaMap = TelegramMediaMap(latitude: updated.latitude, longitude: updated.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + let mediaMap = TelegramMediaMap(latitude: updated.latitude, longitude: updated.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: author, text: text, attributes: [], media: [mediaMap], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index a0217da0b5d..5cd1237b3fa 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -67,21 +67,21 @@ public final class ChatSendContactMessageContextPreview: UIView, ChatSendMessage deinit { } - public func animateIn(transition: Transition) { + public func animateIn(transition: ComponentTransition) { transition.animateAlpha(view: self.messagesContainer, from: 0.0, to: 1.0) transition.animateScale(view: self.messagesContainer, from: 0.001, to: 1.0) } - public func animateOut(transition: Transition) { + public func animateOut(transition: ComponentTransition) { transition.setAlpha(view: self.messagesContainer, alpha: 0.0) transition.setScale(view: self.messagesContainer, scale: 0.001) } - public func animateOutOnSend(transition: Transition) { + public func animateOutOnSend(transition: ComponentTransition) { transition.setAlpha(view: self.messagesContainer, alpha: 0.0) } - public func update(containerSize: CGSize, transition: Transition) -> CGSize { + public func update(containerSize: CGSize, transition: ComponentTransition) -> CGSize { var contactsMedia: [TelegramMediaContact] = [] for peer in self.contactPeers { switch peer { @@ -241,21 +241,21 @@ public final class ChatSendAudioMessageContextPreview: UIView, ChatSendMessageCo deinit { } - public func animateIn(transition: Transition) { + public func animateIn(transition: ComponentTransition) { transition.animateAlpha(view: self.messagesContainer, from: 0.0, to: 1.0) transition.animateScale(view: self.messagesContainer, from: 0.001, to: 1.0) } - public func animateOut(transition: Transition) { + public func animateOut(transition: ComponentTransition) { transition.setAlpha(view: self.messagesContainer, alpha: 0.0) transition.setScale(view: self.messagesContainer, scale: 0.001) } - public func animateOutOnSend(transition: Transition) { + public func animateOutOnSend(transition: ComponentTransition) { transition.setAlpha(view: self.messagesContainer, alpha: 0.0) } - public func update(containerSize: CGSize, transition: Transition) -> CGSize { + public func update(containerSize: CGSize, transition: ComponentTransition) -> CGSize { let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: self.waveform.makeBitstream())] let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) @@ -375,21 +375,21 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess deinit { } - public func animateIn(transition: Transition) { + public func animateIn(transition: ComponentTransition) { transition.animateAlpha(view: self.messagesContainer, from: 0.0, to: 1.0) transition.animateScale(view: self.messagesContainer, from: 0.001, to: 1.0) } - public func animateOut(transition: Transition) { + public func animateOut(transition: ComponentTransition) { transition.setAlpha(view: self.messagesContainer, alpha: 0.0) transition.setScale(view: self.messagesContainer, scale: 0.001) } - public func animateOutOnSend(transition: Transition) { + public func animateOutOnSend(transition: ComponentTransition) { transition.setAlpha(view: self.messagesContainer, alpha: 0.0) } - public func update(containerSize: CGSize, transition: Transition) -> CGSize { + public func update(containerSize: CGSize, transition: ComponentTransition) -> CGSize { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let chatPresentationData: ChatPresentationData @@ -428,7 +428,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess return nil }, chatControllerNode: { return nil - }, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in + }, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return .none }, canSendMessages: { @@ -483,7 +483,6 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in }, openStickerEditor: { - }, openPhoneContextMenu: { _ in }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in @@ -620,7 +619,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess let prepareLayout = messageNode.asyncLayoutContent() - let prepareContentPosition: ChatMessageBubblePreparePosition = .mosaic(top: .None(.None(.Incoming)), bottom: i == (items.count - 1 - 1) ? bottomPosition : .None(.None(.Incoming))) + let prepareContentPosition: ChatMessageBubblePreparePosition = .mosaic(top: .None(.None(.Incoming)), bottom: i == (items.count - 1 - 1) ? bottomPosition : .None(.None(.Incoming)), index: i) let (properties, unboundSize, maxNodeWidth, nodeLayout) = prepareLayout(items[i], layoutConstants, prepareContentPosition, nil, CGSize(width: maximumContentWidth, height: CGFloat.greatestFiniteMagnitude), 0.0) maximumNodeWidth = min(maximumNodeWidth, maxNodeWidth) @@ -769,7 +768,8 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess let messagesContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) self.messagesContainer.frame = messagesContainerFrame - return messagesContainerFrame.size + // 4.0 is a magic number to compensate for offset in other types of content + return CGSize(width: messagesContainerFrame.width, height: messagesContainerFrame.height - 4.0) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD new file mode 100644 index 00000000000..5d003e453bc --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD @@ -0,0 +1,38 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatSendStarsScreen", + module_name = "ChatSendStarsScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/ChatPresentationInterfaceState", + "//submodules/AccountContext", + "//submodules/ComponentFlow", + "//submodules/ContextUI", + "//submodules/AppBundle", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/Markdown", + "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/SliderComponent", + "//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath", + "//submodules/AvatarNode", + "//submodules/Components/BundleIconComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/BadgeLabelView.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/BadgeLabelView.swift new file mode 100644 index 00000000000..6e24d18396c --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/BadgeLabelView.swift @@ -0,0 +1,166 @@ +import Foundation +import UIKit +import Display +import ComponentFlow + +private let labelWidth: CGFloat = 16.0 +private let labelHeight: CGFloat = 36.0 +private let labelSize = CGSize(width: labelWidth, height: labelHeight) +private let font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: []) + +final class BadgeLabelView: UIView { + private class StackView: UIView { + var labels: [UILabel] = [] + + var currentValue: Int32 = 0 + + var color: UIColor = .white { + didSet { + for view in self.labels { + view.textColor = self.color + } + } + } + + init() { + super.init(frame: CGRect(origin: .zero, size: labelSize)) + + var height: CGFloat = -labelHeight + for i in -1 ..< 10 { + let label = UILabel() + if i == -1 { + label.text = "9" + } else { + label.text = "\(i)" + } + label.textColor = self.color + label.font = font + label.textAlignment = .center + label.frame = CGRect(x: 0, y: height, width: labelWidth, height: labelHeight) + self.addSubview(label) + self.labels.append(label) + + height += labelHeight + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(value: Int32, isFirst: Bool, isLast: Bool, transition: ComponentTransition) { + let previousValue = self.currentValue + self.currentValue = value + + self.labels[1].alpha = isFirst && !isLast ? 0.0 : 1.0 + + if previousValue == 9 && value < 9 { + self.bounds = CGRect( + origin: CGPoint( + x: 0.0, + y: -1.0 * labelSize.height + ), + size: labelSize + ) + } + + let bounds = CGRect( + origin: CGPoint( + x: 0.0, + y: CGFloat(value) * labelSize.height + ), + size: labelSize + ) + transition.setBounds(view: self, bounds: bounds) + } + } + + private var itemViews: [Int: StackView] = [:] + private var staticLabel = UILabel() + + init() { + super.init(frame: .zero) + + self.clipsToBounds = true + self.isUserInteractionEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var color: UIColor = .white { + didSet { + self.staticLabel.textColor = self.color + for (_, view) in self.itemViews { + view.color = self.color + } + } + } + + func update(value: String, transition: ComponentTransition) -> CGSize { + if value.contains(" ") { + for (_, view) in self.itemViews { + view.isHidden = true + } + + if self.staticLabel.superview == nil { + self.staticLabel.textColor = self.color + self.staticLabel.font = font + + self.addSubview(self.staticLabel) + } + + self.staticLabel.text = value + let size = self.staticLabel.sizeThatFits(CGSize(width: 100.0, height: 100.0)) + self.staticLabel.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: labelHeight)) + + return CGSize(width: ceil(self.staticLabel.bounds.width), height: ceil(self.staticLabel.bounds.height)) + } + + let string = value + let stringArray = Array(string.map { String($0) }.reversed()) + + let totalWidth = CGFloat(stringArray.count) * labelWidth + + var validIds: [Int] = [] + for i in 0 ..< stringArray.count { + validIds.append(i) + + let itemView: StackView + var itemTransition = transition + if let current = self.itemViews[i] { + itemView = current + } else { + itemTransition = transition.withAnimation(.none) + itemView = StackView() + itemView.color = self.color + self.itemViews[i] = itemView + self.addSubview(itemView) + } + + let digit = Int32(stringArray[i]) ?? 0 + itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition) + + itemTransition.setFrame( + view: itemView, + frame: CGRect(x: totalWidth - labelWidth * CGFloat(i + 1), y: 0.0, width: labelWidth, height: labelHeight) + ) + } + + var removeIds: [Int] = [] + for (id, itemView) in self.itemViews { + if !validIds.contains(id) { + removeIds.append(id) + + transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in + itemView.removeFromSuperview() + }) + } + } + for id in removeIds { + self.itemViews.removeValue(forKey: id) + } + return CGSize(width: totalWidth, height: labelHeight) + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift new file mode 100644 index 00000000000..d6556c800c0 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -0,0 +1,1439 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import TelegramPresentationData +import ChatPresentationInterfaceState +import ComponentFlow +import AccountContext +import ViewControllerComponent +import TelegramCore +import SwiftSignalKit +import Display +import MultilineTextComponent +import ButtonComponent +import PlainButtonComponent +import Markdown +import EmojiStatusComponent +import SliderComponent +import RoundedRectWithTailPath +import AvatarNode +import BundleIconComponent + +private final class BalanceComponent: CombinedComponent { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let balance: Int64? + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + balance: Int64? + ) { + self.context = context + self.theme = theme + self.strings = strings + self.balance = balance + } + + static func ==(lhs: BalanceComponent, rhs: BalanceComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.balance != rhs.balance { + return false + } + return true + } + + static var body: Body { + let title = Child(MultilineTextComponent.self) + let balance = Child(MultilineTextComponent.self) + let icon = Child(EmojiStatusComponent.self) + + return { context in + var size = CGSize(width: 0.0, height: 0.0) + + //TODO:localize + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: "Balance", font: Font.regular(14.0), textColor: context.component.theme.list.itemPrimaryTextColor)) + ), + availableSize: context.availableSize, + transition: .immediate + ) + + size.width = max(size.width, title.size.width) + size.height += title.size.height + + let balanceText: String + if let value = context.component.balance { + balanceText = "\(value)" + } else { + balanceText = "..." + } + let balance = balance.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: balanceText, font: Font.medium(15.0), textColor: context.component.theme.list.itemPrimaryTextColor)) + ), + availableSize: context.availableSize, + transition: .immediate + ) + + let iconSize = CGSize(width: 18.0, height: 18.0) + let icon = icon.update( + component: EmojiStatusComponent( + context: context.component.context, + animationCache: context.component.context.animationCache, + animationRenderer: context.component.context.animationRenderer, + content: .animation( + content: .customEmoji(fileId: MessageReaction.starsReactionId), + size: iconSize, + placeholderColor: .gray, + themeColor: nil, + loopMode: .count(0) + ), + isVisibleForAnimations: true, + action: nil + ), + availableSize: iconSize, + transition: context.transition + ) + + let titleSpacing: CGFloat = 1.0 + let iconSpacing: CGFloat = 2.0 + + size.height += titleSpacing + + size.width = max(size.width, icon.size.width + iconSpacing + balance.size.width) + size.height += balance.size.height + + context.add( + title.position( + title.size.centered(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: title.size)).center + ) + ) + context.add( + balance.position( + balance.size.centered(in: CGRect(origin: CGPoint(x: icon.size.width + iconSpacing, y: title.size.height + titleSpacing), size: balance.size)).center + ) + ) + context.add( + icon.position( + icon.size.centered(in: CGRect(origin: CGPoint(x: 0.0, y: title.size.height + titleSpacing), size: icon.size)).center + ) + ) + + return size + } + } +} + +private final class BadgeComponent: Component { + let theme: PresentationTheme + let title: String + + init( + theme: PresentationTheme, + title: String + ) { + self.theme = theme + self.title = title + } + + static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + final class View: UIView { + private let badgeView: UIView + private let badgeMaskView: UIView + private let badgeShapeLayer = SimpleShapeLayer() + + private let badgeForeground: SimpleLayer + private let badgeIcon: UIImageView + private let badgeLabel: BadgeLabelView + private let badgeLabelMaskView = UIImageView() + + private var badgeTailPosition: CGFloat = 0.0 + private var badgeShapeArguments: (Double, Double, CGSize, CGFloat, CGFloat)? + + private var component: BadgeComponent? + + private var previousAvailableSize: CGSize? + + override init(frame: CGRect) { + self.badgeView = UIView() + self.badgeView.alpha = 0.0 + + self.badgeShapeLayer.fillColor = UIColor.white.cgColor + self.badgeShapeLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + + self.badgeMaskView = UIView() + self.badgeMaskView.layer.addSublayer(self.badgeShapeLayer) + self.badgeView.mask = self.badgeMaskView + + self.badgeForeground = SimpleLayer() + + self.badgeIcon = UIImageView() + self.badgeIcon.contentMode = .center + + self.badgeLabel = BadgeLabelView() + let _ = self.badgeLabel.update(value: "0", transition: .immediate) + self.badgeLabel.mask = self.badgeLabelMaskView + + super.init(frame: frame) + + self.addSubview(self.badgeView) + self.badgeView.layer.addSublayer(self.badgeForeground) + self.badgeView.addSubview(self.badgeIcon) + self.badgeView.addSubview(self.badgeLabel) + + self.badgeLabelMaskView.contentMode = .scaleToFill + self.badgeLabelMaskView.image = generateImage(CGSize(width: 2.0, height: 36.0), rotatedContext: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + + let colorsArray: [CGColor] = [ + UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, + UIColor(rgb: 0xffffff).cgColor, + UIColor(rgb: 0xffffff).cgColor, + UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, + ] + var locations: [CGFloat] = [0.0, 0.24, 0.76, 1.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + + self.isUserInteractionEnabled = false + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + + if self.component == nil { + self.badgeIcon.image = UIImage(bundleImageName: "Premium/SendStarsStarSliderIcon")?.withRenderingMode(.alwaysTemplate) + } + + self.component = component + self.badgeIcon.tintColor = .white + + self.badgeLabel.color = .white + + let countWidth: CGFloat + switch component.title.count { + case 1: + countWidth = 20.0 + case 2: + countWidth = 35.0 + case 3: + countWidth = 51.0 + case 4: + countWidth = 60.0 + case 5: + countWidth = 74.0 + case 6: + countWidth = 88.0 + default: + countWidth = 51.0 + } + let badgeWidth: CGFloat = countWidth + 54.0 + + let badgeSize = CGSize(width: badgeWidth, height: 48.0) + let badgeFullSize = CGSize(width: badgeWidth, height: 48.0 + 12.0) + self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize) + self.badgeShapeLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: badgeFullSize) + + self.badgeView.bounds = CGRect(origin: .zero, size: badgeFullSize) + + transition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0)) + + self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeFullSize.width * 3.0, height: badgeFullSize.height)) + if self.badgeForeground.animation(forKey: "movement") == nil { + self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeFullSize.height / 2.0) + } + + self.badgeIcon.frame = CGRect(x: 10.0, y: 9.0, width: 30.0, height: 30.0) + self.badgeLabelMaskView.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 36.0) + + self.badgeView.alpha = 1.0 + + let size = badgeSize + + let badgeLabelSize = self.badgeLabel.update(value: component.title, transition: .easeInOut(duration: 0.12)) + transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: 14.0 + floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: 5.0), size: badgeLabelSize)) + + if self.previousAvailableSize != availableSize { + self.previousAvailableSize = availableSize + + let activeColors: [UIColor] = [ + UIColor(rgb: 0xFFAB03), + UIColor(rgb: 0xFFCB37) + ] + + var locations: [CGFloat] = [] + let delta = 1.0 / CGFloat(activeColors.count - 1) + for i in 0 ..< activeColors.count { + locations.append(delta * CGFloat(i)) + } + + let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: activeColors, locations: locations, direction: .horizontal) + self.badgeForeground.contentsGravity = .resizeAspectFill + self.badgeForeground.contents = gradient?.cgImage + + self.setupGradientAnimations() + } + + return size + } + + func adjustTail(size: CGSize, overflowWidth: CGFloat) { + var tailPosition = size.width * 0.5 + tailPosition += overflowWidth + tailPosition = max(0.0, min(size.width, tailPosition)) + + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: size, tailPosition: tailPosition / size.width).cgPath + } + + private func setupGradientAnimations() { + guard let _ = self.component else { + return + } + if let _ = self.badgeForeground.animation(forKey: "movement") { + } else { + CATransaction.begin() + + let badgeOffset = (self.badgeForeground.frame.width - self.badgeView.bounds.width) / 2.0 + let badgePreviousValue = self.badgeForeground.position.x + var badgeNewValue: CGFloat = badgeOffset + if badgeOffset - badgePreviousValue < self.badgeForeground.frame.width * 0.25 { + badgeNewValue -= self.badgeForeground.frame.width * 0.35 + } + self.badgeForeground.position = CGPoint(x: badgeNewValue, y: self.badgeForeground.bounds.size.height / 2.0) + + let badgeAnimation = CABasicAnimation(keyPath: "position.x") + badgeAnimation.duration = 4.5 + badgeAnimation.fromValue = badgePreviousValue + badgeAnimation.toValue = badgeNewValue + badgeAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + + CATransaction.setCompletionBlock { [weak self] in + self?.setupGradientAnimations() + } + self.badgeForeground.add(badgeAnimation, forKey: "movement") + + CATransaction.commit() + } + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class PeerBadgeComponent: Component { + let theme: PresentationTheme + let title: String + + init( + theme: PresentationTheme, + title: String + ) { + self.theme = theme + self.title = title + } + + static func ==(lhs: PeerBadgeComponent, rhs: PeerBadgeComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + final class View: UIView { + private let backgroundMaskLayer = SimpleLayer() + private let backgroundLayer = SimpleLayer() + private let title = ComponentView() + private let icon = ComponentView() + + private var component: PeerBadgeComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.addSublayer(self.backgroundMaskLayer) + self.layer.addSublayer(self.backgroundLayer) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func update(component: PeerBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: "Premium/SendStarsPeerBadgeStarIcon", + tintColor: .white) + ), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let sideInset: CGFloat = 3.0 + let titleSpacing: CGFloat = 1.0 + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.bold(9.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - titleSpacing - iconSize.width, height: 100.0) + ) + + let contentSize = CGSize(width: iconSize.width + titleSpacing + titleSize.width, height: titleSize.height) + let size = CGSize(width: contentSize.width + sideInset * 2.0, height: contentSize.height + 3.0 * 2.0) + + self.backgroundMaskLayer.backgroundColor = component.theme.list.plainBackgroundColor.cgColor + self.backgroundLayer.backgroundColor = UIColor(rgb: 0xFFB10D).cgColor + + let backgroundFrame = CGRect(origin: CGPoint(), size: size) + self.backgroundLayer.frame = backgroundFrame + + let badkgroundMaskFrame = backgroundFrame.insetBy(dx: -1.0 - UIScreenPixel, dy: -1.0 - UIScreenPixel) + self.backgroundMaskLayer.frame = badkgroundMaskFrame + + self.backgroundLayer.cornerRadius = backgroundFrame.height * 0.5 + self.backgroundMaskLayer.cornerRadius = badkgroundMaskFrame.height * 0.5 + + let titleFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + sideInset + iconSize.width + titleSpacing, y: floor((backgroundFrame.height - titleSize.height) * 0.5)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + sideInset + 1.0, y: floor((backgroundFrame.height - iconSize.height) * 0.5)), size: iconSize) + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconView.frame = iconFrame + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class PeerComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let peer: EnginePeer + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + peer: EnginePeer + ) { + self.context = context + self.theme = theme + self.strings = strings + self.peer = peer + } + + static func ==(lhs: PeerComponent, rhs: PeerComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + final class View: UIView { + private var avatarNode: AvatarNode? + private let badge = ComponentView() + private let title = ComponentView() + + private var component: PeerComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func update(component: PeerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 24.0)) + self.avatarNode = avatarNode + self.addSubview(avatarNode.view) + } + + let avatarSize = CGSize(width: 60.0, height: 60.0) + let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: avatarSize) + avatarNode.frame = avatarFrame + avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer) + avatarNode.updateSize(size: avatarFrame.size) + + let badgeSize = self.badge.update( + transition: .immediate, + component: AnyComponent(PeerBadgeComponent( + theme: component.theme, + title: "800" + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 200.0) + ) + let badgeFrame = CGRect(origin: CGPoint(x: avatarFrame.minX + floor((avatarFrame.width - badgeSize.width) * 0.5), y: avatarFrame.maxY - badgeSize.height + 3.0), size: badgeSize) + if let badgeView = self.badge.view { + if badgeView.superview == nil { + self.addSubview(badgeView) + } + badgeView.frame = badgeFrame + } + + let titleSpacing: CGFloat = 8.0 + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.peer.compactDisplayTitle, font: Font.regular(11.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: avatarSize.width + 10.0 * 2.0, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((avatarSize.width - titleSize.width) * 0.5), y: avatarSize.height + titleSpacing), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + return CGSize(width: avatarSize.width, height: avatarSize.height + titleSpacing + titleSize.height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class ChatSendStarsScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let peer: EnginePeer + let balance: Int64? + let topPeers: [EnginePeer] + let completion: (Int64) -> Void + + init( + context: AccountContext, + peer: EnginePeer, + balance: Int64?, + topPeers: [EnginePeer], + completion: @escaping (Int64) -> Void + ) { + self.context = context + self.peer = peer + self.balance = balance + self.topPeers = topPeers + self.completion = completion + } + + static func ==(lhs: ChatSendStarsScreenComponent, rhs: ChatSendStarsScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.balance != rhs.balance { + return false + } + if lhs.topPeers != rhs.topPeers { + return false + } + return true + } + + private struct ItemLayout: Equatable { + var containerSize: CGSize + var containerInset: CGFloat + var bottomInset: CGFloat + var topInset: CGFloat + + init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { + self.containerSize = containerSize + self.containerInset = containerInset + self.bottomInset = bottomInset + self.topInset = topInset + } + } + + private final class ScrollView: UIScrollView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + } + + final class View: UIView, UIScrollViewDelegate { + private let dimView: UIView + private let backgroundLayer: SimpleLayer + private let navigationBarContainer: SparseContainerView + private let scrollView: ScrollView + private let scrollContentClippingView: SparseContainerView + private let scrollContentView: UIView + + private let leftButton = ComponentView() + private let closeButton = ComponentView() + + private let title = ComponentView() + private let descriptionText = ComponentView() + + private let slider = ComponentView() + private let sliderBackground = UIView() + private let sliderForeground = UIView() + private let badge = ComponentView() + + private var topPeersLeftSeparator: SimpleLayer? + private var topPeersRightSeparator: SimpleLayer? + private var topPeersTitleBackground: SimpleLayer? + private var topPeersTitle: ComponentView? + + private var topPeerItems: [EnginePeer.Id: ComponentView] = [:] + + private let actionButton = ComponentView() + private let buttonDescriptionText = ComponentView() + + private let bottomOverscrollLimit: CGFloat + + private var ignoreScrolling: Bool = false + + private var component: ChatSendStarsScreenComponent? + private weak var state: EmptyComponentState? + private var environment: ViewControllerComponentContainer.Environment? + private var itemLayout: ItemLayout? + + private var topOffsetDistance: CGFloat? + + private var amount: Int64 = 1 + private var cachedStarImage: (UIImage, PresentationTheme)? + private var cachedCloseImage: UIImage? + + override init(frame: CGRect) { + self.bottomOverscrollLimit = 200.0 + + self.dimView = UIView() + + self.backgroundLayer = SimpleLayer() + self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.backgroundLayer.cornerRadius = 10.0 + + self.navigationBarContainer = SparseContainerView() + + self.scrollView = ScrollView() + + self.scrollContentClippingView = SparseContainerView() + self.scrollContentClippingView.clipsToBounds = true + + self.scrollContentView = UIView() + + super.init(frame: frame) + + self.addSubview(self.dimView) + self.layer.addSublayer(self.backgroundLayer) + + self.addSubview(self.navigationBarContainer) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.alwaysBounceVertical = true + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + + self.addSubview(self.scrollContentClippingView) + self.scrollContentClippingView.addSubview(self.scrollView) + + self.scrollView.addSubview(self.scrollContentView) + + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + guard let itemLayout = self.itemLayout, let topOffsetDistance = self.topOffsetDistance else { + return + } + + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + topOffset = max(0.0, topOffset) + + if topOffset < topOffsetDistance { + targetContentOffset.pointee.y = scrollView.contentOffset.y + scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if !self.backgroundLayer.frame.contains(point) { + return self.dimView + } + + if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { + return result + } + + let result = super.hitTest(point, with: event) + return result + } + + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + guard let environment = self.environment, let controller = environment.controller() else { + return + } + controller.dismiss() + } + } + + private func updateScrolling(transition: ComponentTransition) { + guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { + return + } + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + topOffset = max(0.0, topOffset) + transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) + + transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) + + let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) + self.topOffsetDistance = topOffsetDistance + var topOffsetFraction = topOffset / topOffsetDistance + topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) + + let transitionFactor: CGFloat = 1.0 - topOffsetFraction + controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) + } + + func animateIn() { + self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + if let actionButtonView = self.actionButton.view { + actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + if let buttonDescriptionTextView = self.buttonDescriptionText.view { + buttonDescriptionTextView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + } + + func animateOut(completion: @escaping () -> Void) { + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + + self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + if let actionButtonView = self.actionButton.view { + actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + } + if let buttonDescriptionTextView = self.buttonDescriptionText.view { + buttonDescriptionTextView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + } + } + + func update(component: ChatSendStarsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let themeUpdated = self.environment?.theme !== environment.theme + + let resetScrolling = self.scrollView.bounds.width != availableSize.width + + let sideInset: CGFloat = 16.0 + + if self.component == nil { + self.amount = 1 + } + + self.component = component + self.state = state + self.environment = environment + + if themeUpdated { + self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.backgroundLayer.backgroundColor = environment.theme.list.plainBackgroundColor.cgColor + + var locations: [NSNumber] = [] + var colors: [CGColor] = [] + let numStops = 6 + for i in 0 ..< numStops { + let step = CGFloat(i) / CGFloat(numStops - 1) + locations.append(step as NSNumber) + colors.append(environment.theme.list.blocksBackgroundColor.withAlphaComponent(1.0 - step * step).cgColor) + } + } + + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + var contentHeight: CGFloat = 0.0 + + let sliderInset: CGFloat = sideInset + 8.0 + let sliderSize = self.slider.update( + transition: transition, + component: AnyComponent(SliderComponent( + valueCount: 1000, + value: 0, + markPositions: false, + trackBackgroundColor: .clear, + trackForegroundColor: .clear, + knobSize: 26.0, + knobColor: .white, + valueUpdated: { [weak self] value in + guard let self else { + return + } + self.amount = 1 + Int64(value) + self.state?.updated(transition: .immediate) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sliderInset * 2.0, height: 30.0) + ) + let sliderFrame = CGRect(origin: CGPoint(x: sliderInset, y: contentHeight + 127.0), size: sliderSize) + if let sliderView = self.slider.view { + if sliderView.superview == nil { + self.scrollContentView.addSubview(self.sliderBackground) + self.scrollContentView.addSubview(self.sliderForeground) + self.scrollContentView.addSubview(sliderView) + } + transition.setFrame(view: sliderView, frame: sliderFrame) + + self.sliderBackground.backgroundColor = UIColor(rgb: 0xEEEEEF) + self.sliderForeground.backgroundColor = UIColor(rgb: 0xFFB10D) + + let sliderBackgroundFrame = CGRect(origin: CGPoint(x: sliderFrame.minX - 8.0, y: sliderFrame.minY + 7.0), size: CGSize(width: sliderFrame.width + 16.0, height: sliderFrame.height - 14.0)) + transition.setFrame(view: self.sliderBackground, frame: sliderBackgroundFrame) + + let progressFraction: CGFloat = CGFloat(self.amount) / CGFloat(1000 - 1) + let sliderMinWidth = sliderBackgroundFrame.height + let sliderAreaWidth: CGFloat = sliderBackgroundFrame.width - sliderMinWidth + let sliderForegroundFrame = CGRect(origin: CGPoint(x: sliderBackgroundFrame.minX, y: sliderBackgroundFrame.minY), size: CGSize(width: sliderMinWidth + floorToScreenPixels(sliderAreaWidth * progressFraction), height: sliderBackgroundFrame.height)) + transition.setFrame(view: self.sliderForeground, frame: sliderForegroundFrame) + + self.sliderBackground.layer.cornerRadius = sliderBackgroundFrame.height * 0.5 + self.sliderForeground.layer.cornerRadius = sliderBackgroundFrame.height * 0.5 + + self.sliderForeground.isHidden = sliderForegroundFrame.width <= sliderMinWidth + + let badgeSize = self.badge.update( + transition: transition, + component: AnyComponent(BadgeComponent( + theme: environment.theme, title: "\(self.amount)") + ), + environment: {}, + containerSize: CGSize(width: 200.0, height: 200.0) + ) + var badgeFrame = CGRect(origin: CGPoint(x: sliderForegroundFrame.minX + sliderForegroundFrame.width - floorToScreenPixels(sliderMinWidth * 0.5), y: sliderForegroundFrame.minY - 8.0), size: badgeSize) + if let badgeView = self.badge.view as? BadgeComponent.View { + if badgeView.superview == nil { + self.scrollContentView.addSubview(badgeView) + } + + let badgeSideInset = sideInset + 15.0 + + let badgeOverflowWidth: CGFloat + if badgeFrame.minX - badgeSize.width * 0.5 < badgeSideInset { + badgeOverflowWidth = badgeSideInset - (badgeFrame.minX - badgeSize.width * 0.5) + } else if badgeFrame.minX + badgeSize.width * 0.5 > availableSize.width - badgeSideInset { + badgeOverflowWidth = availableSize.width - badgeSideInset - (badgeFrame.minX + badgeSize.width * 0.5) + } else { + badgeOverflowWidth = 0.0 + } + + badgeFrame.origin.x += badgeOverflowWidth + + badgeView.frame = badgeFrame + + badgeView.adjustTail(size: badgeSize, overflowWidth: -badgeOverflowWidth) + } + } + + contentHeight += 123.0 + + let leftButtonSize = self.leftButton.update( + transition: transition, + component: AnyComponent(BalanceComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + balance: component.balance + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 100.0) + ) + let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: floor((56.0 - leftButtonSize.height) * 0.5)), size: leftButtonSize) + if let leftButtonView = self.leftButton.view { + if leftButtonView.superview == nil { + self.navigationBarContainer.addSubview(leftButtonView) + } + transition.setFrame(view: leftButtonView, frame: leftButtonFrame) + } + + if themeUpdated { + self.cachedCloseImage = nil + } + let closeImage: UIImage + if let current = self.cachedCloseImage { + closeImage = current + } else { + closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: environment.theme.actionSheet.inputClearButtonColor)! + self.cachedCloseImage = closeImage + } + let closeButtonSize = self.closeButton.update( + transition: .immediate, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(Image(image: closeImage)), + effectAlignment: .center, + action: { [weak self] in + guard let self else { + return + } + self.environment?.controller()?.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: 30.0, height: 30.0) + ) + let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - closeButtonSize.width, y: floor((56.0 - leftButtonSize.height) * 0.5)), size: closeButtonSize) + if let closeButtonView = self.closeButton.view { + if closeButtonView.superview == nil { + self.navigationBarContainer.addSubview(closeButtonView) + } + transition.setFrame(view: closeButtonView, frame: closeButtonFrame) + } + + let containerInset: CGFloat = environment.statusBarHeight + 10.0 + + var initialContentHeight = contentHeight + let clippingY: CGFloat + + let title = self.title + let descriptionText = self.descriptionText + let actionButton = self.actionButton + + let titleSize = title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "React with Stars", font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((56.0 - titleSize.height) * 0.5)), size: titleSize) + if let titleView = title.view { + if titleView.superview == nil { + self.navigationBarContainer.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + contentHeight += 56.0 + contentHeight += 8.0 + + let text = "Choose how many stars you want to send to **\(component.peer.debugDisplayTitle)** to support this post." + + let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor) + let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor) + + let descriptionTextSize = descriptionText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .markdown(text: text, attributes: MarkdownAttributes( + body: body, + bold: bold, + link: body, + linkAttribute: { _ in nil } + )), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0) + ) + let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize) + if let descriptionTextView = descriptionText.view { + if descriptionTextView.superview == nil { + self.scrollContentView.addSubview(descriptionTextView) + } + transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame) + } + + contentHeight += descriptionTextFrame.height + contentHeight += 22.0 + contentHeight += 2.0 + + if !component.topPeers.isEmpty { + contentHeight += 3.0 + + let topPeersLeftSeparator: SimpleLayer + if let current = self.topPeersLeftSeparator { + topPeersLeftSeparator = current + } else { + topPeersLeftSeparator = SimpleLayer() + self.topPeersLeftSeparator = topPeersLeftSeparator + self.scrollContentView.layer.addSublayer(topPeersLeftSeparator) + } + + let topPeersRightSeparator: SimpleLayer + if let current = self.topPeersRightSeparator { + topPeersRightSeparator = current + } else { + topPeersRightSeparator = SimpleLayer() + self.topPeersRightSeparator = topPeersRightSeparator + self.scrollContentView.layer.addSublayer(topPeersRightSeparator) + } + + let topPeersTitleBackground: SimpleLayer + if let current = self.topPeersTitleBackground { + topPeersTitleBackground = current + } else { + topPeersTitleBackground = SimpleLayer() + self.topPeersTitleBackground = topPeersTitleBackground + self.scrollContentView.layer.addSublayer(topPeersTitleBackground) + } + + let topPeersTitle: ComponentView + if let current = self.topPeersTitle { + topPeersTitle = current + } else { + topPeersTitle = ComponentView() + self.topPeersTitle = topPeersTitle + } + + topPeersLeftSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor + topPeersRightSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor + + //TODO:localize + let topPeersTitleSize = topPeersTitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Top Senders", font: Font.semibold(15.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: 300.0, height: 100.0) + ) + let topPeersBackgroundSize = CGSize(width: topPeersTitleSize.width + 16.0 * 2.0, height: topPeersTitleSize.height + 9.0 * 2.0) + let topPeersBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - topPeersBackgroundSize.width) * 0.5), y: contentHeight), size: topPeersBackgroundSize) + + topPeersTitleBackground.backgroundColor = UIColor(rgb: 0xFFB10D).cgColor + topPeersTitleBackground.cornerRadius = topPeersBackgroundFrame.height * 0.5 + transition.setFrame(layer: topPeersTitleBackground, frame: topPeersBackgroundFrame) + + let topPeersTitleFrame = CGRect(origin: CGPoint(x: topPeersBackgroundFrame.minX + floor((topPeersBackgroundFrame.width - topPeersTitleSize.width) * 0.5), y: topPeersBackgroundFrame.minY + floor((topPeersBackgroundFrame.height - topPeersTitleSize.height) * 0.5)), size: topPeersTitleSize) + if let topPeersTitleView = topPeersTitle.view { + if topPeersTitleView.superview == nil { + self.scrollContentView.addSubview(topPeersTitleView) + } + transition.setFrame(view: topPeersTitleView, frame: topPeersTitleFrame) + } + + let separatorY = topPeersBackgroundFrame.midY + let separatorSpacing: CGFloat = 10.0 + transition.setFrame(layer: topPeersLeftSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: separatorY), size: CGSize(width: max(0.0, topPeersBackgroundFrame.minX - separatorSpacing - sideInset), height: UIScreenPixel))) + transition.setFrame(layer: topPeersRightSeparator, frame: CGRect(origin: CGPoint(x: topPeersBackgroundFrame.maxX + separatorSpacing, y: separatorY), size: CGSize(width: max(0.0, availableSize.width - sideInset - (topPeersBackgroundFrame.maxX + separatorSpacing)), height: UIScreenPixel))) + + var validIds: [EnginePeer.Id] = [] + var items: [(itemView: ComponentView, size: CGSize)] = [] + for topPeer in component.topPeers { + validIds.append(topPeer.id) + + let itemView: ComponentView + if let current = self.topPeerItems[topPeer.id] { + itemView = current + } else { + itemView = ComponentView() + self.topPeerItems[topPeer.id] = itemView + } + + let itemSize = itemView.update( + transition: .immediate, + component: AnyComponent(PeerComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: topPeer + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 200.0) + ) + items.append((itemView, itemSize)) + } + var removedIds: [EnginePeer.Id] = [] + for (id, itemView) in self.topPeerItems { + if !validIds.contains(id) { + removedIds.append(id) + itemView.view?.removeFromSuperview() + } + } + for id in removedIds { + self.topPeerItems.removeValue(forKey: id) + } + + var itemsWidth: CGFloat = 0.0 + for (_, itemSize) in items { + itemsWidth += itemSize.width + } + + let maxItemSpacing = 48.0 + var itemSpacing = floor((availableSize.width - itemsWidth) / CGFloat(items.count + 1)) + itemSpacing = min(itemSpacing, maxItemSpacing) + + let totalWidth = itemsWidth + itemSpacing * CGFloat(items.count + 1) + var itemX: CGFloat = floor((availableSize.width - totalWidth) * 0.5) + itemSpacing + for (itemView, itemSize) in items { + if let itemComponentView = itemView.view { + if itemComponentView.superview == nil { + self.scrollContentView.addSubview(itemComponentView) + } + itemComponentView.frame = CGRect(origin: CGPoint(x: itemX, y: contentHeight + 56.0), size: itemSize) + } + itemX += itemSize.width + itemSpacing + } + + contentHeight += 161.0 + } + + initialContentHeight = contentHeight + + if self.cachedStarImage == nil || self.cachedStarImage?.1 !== environment.theme { + self.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, environment.theme) + } + + let buttonString = "Send # \(self.amount)" + let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) + if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.cachedStarImage?.0 { + buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string)) + } + + let actionButtonSize = actionButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.completion(self.amount) + self.environment?.controller()?.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + + let buttonDescriptionTextSize = self.buttonDescriptionText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .markdown(text: "By sending Stars you agree to the [Terms of Service]()", attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemSecondaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { url in + return ("URL", url) + } + )), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset, height: 1000.0) + ) + let buttonDescriptionSpacing: CGFloat = 14.0 + + let bottomPanelHeight = 13.0 + environment.safeInsets.bottom + actionButtonSize.height + buttonDescriptionSpacing + buttonDescriptionTextSize.height + let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) + if let actionButtonView = actionButton.view { + if actionButtonView.superview == nil { + self.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + } + + let buttonDescriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonDescriptionTextSize.width) * 0.5), y: actionButtonFrame.maxY + buttonDescriptionSpacing), size: buttonDescriptionTextSize) + if let buttonDescriptionTextView = buttonDescriptionText.view { + if buttonDescriptionTextView.superview == nil { + self.addSubview(buttonDescriptionTextView) + } + transition.setFrame(view: buttonDescriptionTextView, frame: buttonDescriptionTextFrame) + } + + contentHeight += bottomPanelHeight + initialContentHeight += bottomPanelHeight + + clippingY = actionButtonFrame.minY - 24.0 + + let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) + + let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) + + self.scrollContentClippingView.layer.cornerRadius = 10.0 + + self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset) + + transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) + + transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) + transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) + + let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset)) + transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) + transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) + + self.ignoreScrolling = true + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) + let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) + if contentSize != self.scrollView.contentSize { + self.scrollView.contentSize = contentSize + } + if resetScrolling { + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) + } + self.ignoreScrolling = false + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class ChatSendStarsScreen: ViewControllerComponentContainer { + public final class InitialData { + let peer: EnginePeer + let balance: Int64? + let topPeers: [EnginePeer] + + fileprivate init( + peer: EnginePeer, + balance: Int64?, + topPeers: [EnginePeer] + ) { + self.peer = peer + self.balance = balance + self.topPeers = topPeers + } + } + + private let context: AccountContext + + private var isDismissed: Bool = false + + private var presenceDisposable: Disposable? + + public init(context: AccountContext, initialData: InitialData, completion: @escaping (Int64) -> Void) { + self.context = context + + super.init(context: context, component: ChatSendStarsScreenComponent( + context: context, + peer: initialData.peer, + balance: initialData.balance, + topPeers: initialData.topPeers, + completion: completion + ), navigationBarAppearance: .none) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presenceDisposable?.dispose() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + + if let componentView = self.node.hostView.componentView as? ChatSendStarsScreenComponent.View { + componentView.animateIn() + } + } + + public static func initialData(context: AccountContext, peerId: EnginePeer.Id) -> Signal { + let balance: Signal + if let starsContext = context.starsContext { + balance = starsContext.state + |> map { state in + return state?.balance + } + |> take(1) + } else { + balance = .single(nil) + } + + return combineLatest( + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)), + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), + balance + ) + |> map { peer, accountPeer, balance -> InitialData? in + guard let peer, let accountPeer else { + return nil + } + + return InitialData( + peer: peer, + balance: balance, + topPeers: [accountPeer, peer] + ) + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + + if let componentView = self.node.hostView.componentView as? ChatSendStarsScreenComponent.View { + componentView.animateOut(completion: { [weak self] in + completion?() + self?.dismiss(animated: false) + }) + } else { + self.dismiss(animated: false) + } + } + } +} + +private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(foregroundColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} diff --git a/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift b/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift index 080aeb97a62..869462e11b7 100644 --- a/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift +++ b/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift @@ -255,7 +255,15 @@ public func topMessageReactions(context: AccountContext, message: Message, subPe guard let allowedReactions = allowedReactions else { return .single(nil) } + if case let .set(reactions) = allowedReactions { + #if DEBUG + var reactions = reactions + if context.sharedContext.applicationBindings.appBuildType == .internal { + reactions.insert(.custom(MessageReaction.starsReactionId)) + } + #endif + return context.engine.stickers.resolveInlineStickers(fileIds: reactions.compactMap { item -> Int64? in switch item { case .builtin: @@ -265,9 +273,17 @@ public func topMessageReactions(context: AccountContext, message: Message, subPe } }) |> map { files -> (reactions: AllowedReactions, files: [Int64: TelegramMediaFile]) in - return (allowedReactions, files) + return (.set(reactions), files) } } else { + #if DEBUG + if context.sharedContext.applicationBindings.appBuildType == .internal { + return context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId]) + |> map { files -> (reactions: AllowedReactions, files: [Int64: TelegramMediaFile]) in + return (allowedReactions, files) + } + } + #endif return .single((allowedReactions, [:])) } } @@ -286,6 +302,25 @@ public func topMessageReactions(context: AccountContext, message: Message, subPe var result: [ReactionItem] = [] var existingIds = Set() + #if DEBUG + if context.sharedContext.applicationBindings.appBuildType == .internal { + if let file = allowedReactionsAndFiles.files[MessageReaction.starsReactionId] { + existingIds.insert(.custom(MessageReaction.starsReactionId)) + + result.append(ReactionItem( + reaction: ReactionItem.Reaction(rawValue: .custom(file.fileId.id)), + appearAnimation: file, + stillAnimation: file, + listAnimation: file, + largeListAnimation: file, + applicationAnimation: nil, + largeApplicationAnimation: nil, + isCustom: true + )) + } + } + #endif + for topReaction in topReactions { switch topReaction.content { case let .builtin(value): diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 66d1984fa35..a6e547d38fc 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -109,10 +109,12 @@ public struct NavigateToMessageParams { public struct OpenMessageParams { public var mode: ChatControllerInteractionOpenMessageMode + public var mediaIndex: Int? public var progress: Promise? - public init(mode: ChatControllerInteractionOpenMessageMode, progress: Promise? = nil) { + public init(mode: ChatControllerInteractionOpenMessageMode, mediaIndex: Int? = nil, progress: Promise? = nil) { self.mode = mode + self.mediaIndex = mediaIndex self.progress = progress } } @@ -154,15 +156,13 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol } } - public struct OpenPhone { - public var number: String - public var message: Message - public var contentNode: ContextExtractedContentContainingNode - public var messageNode: ASDisplayNode + public struct LongTapParams { + public var message: Message? + public var contentNode: ContextExtractedContentContainingNode? + public var messageNode: ASDisplayNode? public var progress: Promise? - public init(number: String, message: Message, contentNode: ContextExtractedContentContainingNode, messageNode: ASDisplayNode, progress: Promise? = nil) { - self.number = number + public init(message: Message? = nil, contentNode: ContextExtractedContentContainingNode? = nil, messageNode: ASDisplayNode? = nil, progress: Promise? = nil) { self.message = message self.contentNode = contentNode self.messageNode = messageNode @@ -210,8 +210,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let chatControllerNode: () -> ASDisplayNode? public let presentGlobalOverlayController: (ViewController, Any?) -> Void public let callPeer: (PeerId, Bool) -> Void - public let longTap: (ChatControllerInteractionLongTapAction, Message?) -> Void - public let openCheckoutOrReceipt: (MessageId) -> Void + public let longTap: (ChatControllerInteractionLongTapAction, LongTapParams?) -> Void + public let openCheckoutOrReceipt: (MessageId, OpenMessageParams?) -> Void public let openSearch: () -> Void public let setupReply: (MessageId) -> Void public let canSetupReply: (Message) -> ChatControllerInteractionSwipeAction @@ -264,7 +264,6 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let openRecommendedChannelContextMenu: (EnginePeer, UIView, ContextGesture?) -> Void public let openGroupBoostInfo: (EnginePeer.Id?, Int) -> Void public let openStickerEditor: () -> Void - public let openPhoneContextMenu: (OpenPhone) -> Void public let openAgeRestrictedMessageMedia: (Message, @escaping () -> Void) -> Void public let playMessageEffect: (Message) -> Void public let editMessageFactCheck: (MessageId) -> Void @@ -343,8 +342,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol chatControllerNode: @escaping () -> ASDisplayNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId, Bool) -> Void, - longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, - openCheckoutOrReceipt: @escaping (MessageId) -> Void, + longTap: @escaping (ChatControllerInteractionLongTapAction, LongTapParams?) -> Void, + openCheckoutOrReceipt: @escaping (MessageId, OpenMessageParams?) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> ChatControllerInteractionSwipeAction, @@ -397,7 +396,6 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol openRecommendedChannelContextMenu: @escaping (EnginePeer, UIView, ContextGesture?) -> Void, openGroupBoostInfo: @escaping (EnginePeer.Id?, Int) -> Void, openStickerEditor: @escaping () -> Void, - openPhoneContextMenu: @escaping (OpenPhone) -> Void, openAgeRestrictedMessageMedia: @escaping (Message, @escaping () -> Void) -> Void, playMessageEffect: @escaping (Message) -> Void, editMessageFactCheck: @escaping (MessageId) -> Void, @@ -509,7 +507,6 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol self.openRecommendedChannelContextMenu = openRecommendedChannelContextMenu self.openGroupBoostInfo = openGroupBoostInfo self.openStickerEditor = openStickerEditor - self.openPhoneContextMenu = openPhoneContextMenu self.openAgeRestrictedMessageMedia = openAgeRestrictedMessageMedia self.playMessageEffect = playMessageEffect self.editMessageFactCheck = editMessageFactCheck diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 2fd1082fb97..00d54572675 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -423,7 +423,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { private var currentState: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, interfaceState: ChatPresentationInterfaceState, layoutMetrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool)? private var scheduledContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? - private var scheduledInnerTransition: Transition? + private var scheduledInnerTransition: ComponentTransition? private var gifMode: GifPagerContentComponent.Subject? { didSet { @@ -1594,7 +1594,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } } - var transition: Transition = .immediate + var transition: ComponentTransition = .immediate var useAnimation = false if let pagerView = strongSelf.entityKeyboardView.componentView as? EntityKeyboardComponent.View, let centralId = pagerView.centralId { @@ -1613,7 +1613,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } else { contentAnimation = EmojiPagerContentComponent.ContentAnimation(type: .generic) } - transition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) + transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) } strongSelf.currentInputData = strongSelf.processInputData(inputData: inputData) strongSelf.performLayout(transition: transition) @@ -1749,7 +1749,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.isMarkInputCollapsed = true } - private func performLayout(transition: Transition) { + private func performLayout(transition: ComponentTransition) { guard let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, layoutMetrics, deviceMetrics, isVisible, isExpanded) = self.currentState else { return } @@ -1767,12 +1767,12 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { public override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, layoutMetrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) { self.currentState = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, layoutMetrics, deviceMetrics, isVisible, isExpanded) - let innerTransition: Transition + let innerTransition: ComponentTransition if let scheduledInnerTransition = self.scheduledInnerTransition { self.scheduledInnerTransition = nil innerTransition = scheduledInnerTransition } else { - innerTransition = Transition(transition) + innerTransition = ComponentTransition(transition) } let wasMarkedInputCollapsed = self.isMarkInputCollapsed @@ -2104,7 +2104,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let strongSelf = self else { return } - strongSelf.performLayout(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + strongSelf.performLayout(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) }) if self.context.sharedContext.currentStickerSettings.with({ $0 }).dynamicPackOrder { @@ -2140,7 +2140,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - let gallery = GalleryController(context: strongSelf.context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in + let gallery = GalleryController(context: strongSelf.context, source: .standaloneMessage(message, nil), streamSingleVideo: true, replaceRootController: { _, _ in }, baseNavigationController: nil) gallery.setHintWillBePresentedInPreviewingContext(true) diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ActionListItemComponent.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ActionListItemComponent.swift index 9e4fa31343a..76319deaf6c 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ActionListItemComponent.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ActionListItemComponent.swift @@ -118,7 +118,7 @@ final class ActionListItemComponent: Component { component.action() } - func update(component: ActionListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ActionListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme if self.component?.iconName != component.iconName { @@ -190,7 +190,7 @@ final class ActionListItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift index b58d9105de1..68da9ab767c 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift @@ -45,7 +45,7 @@ final class BadgeComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let height: CGFloat = 20.0 let contentInset: CGFloat = 10.0 @@ -77,7 +77,7 @@ final class BadgeComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -141,7 +141,7 @@ final class ChatFolderLinkHeaderComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ChatFolderLinkHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChatFolderLinkHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme self.component = component @@ -317,7 +317,7 @@ final class ChatFolderLinkHeaderComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index 976c1e491b8..6567f48f584 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -248,7 +248,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { return } @@ -312,7 +312,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } } - func update(component: ChatFolderLinkPreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChatFolderLinkPreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let animationHint = transition.userData(AnimationHint.self) var contentTransition = transition @@ -623,7 +623,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { if let self, let component = self.component { self.linkListItems.removeAll(where: { $0.link == link.link }) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) let context = component.context let _ = (context.engine.peers.editChatFolderLink(filterId: folderId, link: link, title: nil, peerIds: nil, revoke: true) @@ -728,7 +728,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } else { self.selectedItems.insert(peer.id) } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) } else if linkContents.alreadyMemberPeerIds.contains(peer.id) { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let text: String @@ -744,7 +744,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } else { self.selectedItems.insert(peer.id) } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) } } )), @@ -886,7 +886,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { self.selectedItems.insert(peerId) } } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) } )), environment: {}, @@ -1371,7 +1371,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } self.linkListItems.insert(link, at: 0) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) let navigationController = controller.navigationController controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: { [weak self] updatedLink in @@ -1389,7 +1389,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { self.linkListItems.insert(updatedLink, at: 0) } } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) }, presentController: { [weak navigationController] c in (navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root)) })) @@ -1471,7 +1471,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/LinkListItemComponent.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/LinkListItemComponent.swift index ae5ece266cc..ceafa4284c5 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/LinkListItemComponent.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/LinkListItemComponent.swift @@ -139,11 +139,11 @@ final class LinkListItemComponent: Component { } self.isExtractedToContextMenu = value - let mappedTransition: Transition + let mappedTransition: ComponentTransition if value { - mappedTransition = Transition(transition) + mappedTransition = ComponentTransition(transition) } else { - mappedTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + mappedTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) } self.state?.updated(transition: mappedTransition) } @@ -198,7 +198,7 @@ final class LinkListItemComponent: Component { component.action(component.link) } - func update(component: LinkListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: LinkListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme self.component = component @@ -323,7 +323,7 @@ final class LinkListItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift index 10f12a56343..79c65a9ef12 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift @@ -127,7 +127,7 @@ final class PeerListItemComponent: Component { component.action(peer) } - func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme var hasSelectionUpdated = false @@ -306,7 +306,7 @@ final class PeerListItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index c030552d5eb..732557021e3 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -55,7 +55,7 @@ public final class HeaderNetworkStatusComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: HeaderNetworkStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: HeaderNetworkStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.state = state return availableSize @@ -66,7 +66,7 @@ public final class HeaderNetworkStatusComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -266,7 +266,7 @@ public final class ChatListHeaderComponent: Component { self.onPressed() } - func update(title: String, theme: PresentationTheme, availableSize: CGSize, transition: Transition) -> CGSize { + func update(title: String, theme: PresentationTheme, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let titleText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) let titleTextUpdated = self.titleView.attributedText != titleText self.titleView.attributedText = titleText @@ -380,7 +380,7 @@ public final class ChatListHeaderComponent: Component { return nil } - func updateContentOffsetFraction(contentOffsetFraction: CGFloat, transition: Transition) { + func updateContentOffsetFraction(contentOffsetFraction: CGFloat, transition: ComponentTransition) { if self.contentOffsetFraction == contentOffsetFraction { return } @@ -393,7 +393,7 @@ public final class ChatListHeaderComponent: Component { transition.setSublayerTransform(view: self.titleOffsetContainer, transform: transform) } - func updateNavigationTransitionAsPrevious(nextView: ContentView, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) { + func updateNavigationTransitionAsPrevious(nextView: ContentView, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { transition.setBounds(view: self.leftButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: fraction * self.bounds.width * 0.5, y: 0.0), size: self.leftButtonOffsetContainer.bounds.size), completion: { _ in completion() }) @@ -417,7 +417,7 @@ public final class ChatListHeaderComponent: Component { } } - func updateNavigationTransitionAsNext(previousView: ContentView, storyPeerListView: StoryPeerListComponent.View?, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) { + func updateNavigationTransitionAsNext(previousView: ContentView, storyPeerListView: StoryPeerListComponent.View?, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: -(1.0 - fraction) * self.bounds.width, y: 0.0), size: self.titleOffsetContainer.bounds.size), completion: { _ in completion() }) @@ -447,7 +447,7 @@ public final class ChatListHeaderComponent: Component { } } - func updateNavigationTransitionAsPreviousInplace(nextView: ContentView, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) { + func updateNavigationTransitionAsPreviousInplace(nextView: ContentView, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { transition.setBounds(view: self.leftButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.leftButtonOffsetContainer.bounds.size), completion: { _ in }) transition.setAlpha(view: self.leftButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) @@ -464,7 +464,7 @@ public final class ChatListHeaderComponent: Component { transition.setAlpha(view: self.titleOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) } - func updateNavigationTransitionAsNextInplace(previousView: ContentView, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) { + func updateNavigationTransitionAsNextInplace(previousView: ContentView, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.titleOffsetContainer.bounds.size), completion: { _ in completion() }) @@ -479,7 +479,7 @@ public final class ChatListHeaderComponent: Component { } } - func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, backTitle: String?, sideInset: CGFloat, sideContentWidth: CGFloat, sideContentFraction: CGFloat, size: CGSize, transition: Transition) { + func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, backTitle: String?, sideInset: CGFloat, sideContentWidth: CGFloat, sideContentFraction: CGFloat, size: CGSize, transition: ComponentTransition) { transition.setPosition(view: self.titleOffsetContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: self.titleOffsetContainer.bounds.origin, size: size)) @@ -838,10 +838,10 @@ public final class ChatListHeaderComponent: Component { return defaultResult } - private func updateContentStoryOffsets(transition: Transition) { + private func updateContentStoryOffsets(transition: ComponentTransition) { } - func update(component: ChatListHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChatListHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.state = state let previousComponent = self.component @@ -1108,7 +1108,7 @@ public final class ChatListHeaderComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -1206,7 +1206,7 @@ public final class NavigationButtonComponent: Component { self.component?.pressed(self) } - func update(component: NavigationButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: NavigationButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let theme = environment[NavigationButtonComponentEnvironment.self].value.theme @@ -1356,7 +1356,7 @@ public final class NavigationButtonComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift index d59b4fe7729..549c6fb4fd9 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift @@ -219,13 +219,13 @@ public final class ChatListNavigationBar: Component { return result } - public func applyCurrentScroll(transition: Transition) { + public func applyCurrentScroll(transition: ComponentTransition) { if let rawScrollOffset = self.rawScrollOffset, self.hasDeferredScrollOffset { self.applyScroll(offset: rawScrollOffset, allowAvatarsExpansion: self.currentAllowAvatarsExpansion, transition: transition) } } - public func applyScroll(offset: CGFloat, allowAvatarsExpansion: Bool, forceUpdate: Bool = false, transition: Transition) { + public func applyScroll(offset: CGFloat, allowAvatarsExpansion: Bool, forceUpdate: Bool = false, transition: ComponentTransition) { let transition = transition self.rawScrollOffset = offset @@ -574,7 +574,7 @@ public final class ChatListNavigationBar: Component { } } - func update(component: ChatListNavigationBar, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChatListNavigationBar, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme var uploadProgressUpdated = false @@ -638,7 +638,7 @@ public final class ChatListNavigationBar: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift index d12b91bf48b..7c8a20b0b83 100644 --- a/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift +++ b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift @@ -141,9 +141,9 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation statusContent = .animation(content: .customEmoji(fileId: emoji.fileId), size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2)) } - var titleCredibilityIconTransition: Transition + var titleCredibilityIconTransition: ComponentTransition if animateStatusTransition { - titleCredibilityIconTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + titleCredibilityIconTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) } else { titleCredibilityIconTransition = .immediate } @@ -383,7 +383,7 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation statusContent = .animation(content: .customEmoji(fileId: emoji.fileId), size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2)) } - var titleCredibilityIconTransition = Transition(transition) + var titleCredibilityIconTransition = ComponentTransition(transition) let titleCredibilityIconView: ComponentHostView if let current = self.titleCredibilityIconView { titleCredibilityIconView = current diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index 05a06fd9961..d8474a20731 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -1108,7 +1108,7 @@ public final class ChatTitleComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ChatTitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChatTitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let contentView: ChatTitleView @@ -1157,7 +1157,7 @@ public final class ChatTitleComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuActionNode.swift b/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuActionNode.swift index 4d803b4a960..71fdce0495a 100644 --- a/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuActionNode.swift +++ b/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuActionNode.swift @@ -15,7 +15,9 @@ final private class ContextMenuActionButton: HighlightTrackingButton { final class ContextMenuActionNode: ASDisplayNode { private let textNode: ImmediateTextNode? + private let subtitleNode: ImmediateTextNode? private var textSize: CGSize? + private var subtitleSize: CGSize? private let iconView: UIImageView? private let action: () -> Void private let button: ContextMenuActionButton @@ -28,37 +30,60 @@ final class ContextMenuActionNode: ASDisplayNode { self.actionArea.accessibilityTraits = .button switch action.content { - case let .text(title, accessibilityLabel): - self.actionArea.accessibilityLabel = accessibilityLabel - - let textNode = ImmediateTextNode() - textNode.isUserInteractionEnabled = false - textNode.displaysAsynchronously = false - textNode.attributedText = NSAttributedString(string: title, font: Font.regular(14.0), textColor: isDark ? .white : .black) - textNode.isAccessibilityElement = false - - self.textNode = textNode - self.iconView = nil - case let .textWithIcon(title, icon): - let textNode = ImmediateTextNode() - textNode.isUserInteractionEnabled = false - textNode.displaysAsynchronously = false - textNode.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: isDark ? .white : .black) - textNode.isAccessibilityElement = false + case let .text(title, accessibilityLabel): + self.actionArea.accessibilityLabel = accessibilityLabel - let iconView = UIImageView() - iconView.tintColor = isDark ? .white : .black - iconView.image = icon - - self.textNode = textNode - self.iconView = iconView - case let .icon(image): - let iconView = UIImageView() - iconView.tintColor = isDark ? .white : .black - iconView.image = image - - self.iconView = iconView - self.textNode = nil + let textNode = ImmediateTextNode() + textNode.isUserInteractionEnabled = false + textNode.displaysAsynchronously = false + textNode.attributedText = NSAttributedString(string: title, font: Font.regular(14.0), textColor: isDark ? .white : .black) + textNode.isAccessibilityElement = false + + self.textNode = textNode + self.subtitleNode = nil + self.iconView = nil + case let .textWithIcon(title, icon): + let textNode = ImmediateTextNode() + textNode.isUserInteractionEnabled = false + textNode.displaysAsynchronously = false + textNode.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: isDark ? .white : .black) + textNode.isAccessibilityElement = false + + let iconView = UIImageView() + iconView.tintColor = isDark ? .white : .black + iconView.image = icon + + self.textNode = textNode + self.subtitleNode = nil + self.iconView = iconView + case let .textWithSubtitleAndIcon(title, subtitle, icon): + let textNode = ImmediateTextNode() + textNode.isUserInteractionEnabled = false + textNode.displaysAsynchronously = false + textNode.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: isDark ? .white : .black) + textNode.isAccessibilityElement = false + + let subtitleNode = ImmediateTextNode() + subtitleNode.isUserInteractionEnabled = false + subtitleNode.displaysAsynchronously = false + subtitleNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(12.0), textColor: (isDark ? UIColor.white : UIColor.black).withAlphaComponent(0.5)) + subtitleNode.isAccessibilityElement = false + + let iconView = UIImageView() + iconView.tintColor = isDark ? .white : .black + iconView.image = icon + + self.textNode = textNode + self.subtitleNode = subtitleNode + self.iconView = iconView + case let .icon(image): + let iconView = UIImageView() + iconView.tintColor = isDark ? .white : .black + iconView.image = image + + self.iconView = iconView + self.textNode = nil + self.subtitleNode = nil } self.action = action.action @@ -74,6 +99,9 @@ final class ContextMenuActionNode: ASDisplayNode { if let textNode = self.textNode { self.addSubnode(textNode) } + if let subtitleNode = self.subtitleNode { + self.addSubnode(subtitleNode) + } if let iconView = self.iconView { self.view.addSubview(iconView) } @@ -115,11 +143,21 @@ final class ContextMenuActionNode: ASDisplayNode { override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { if let textNode = self.textNode { + let constrainedSize = CGSize(width: constrainedSize.width - 36.0 - 24.0, height: constrainedSize.height) let textSize = textNode.updateLayout(constrainedSize) self.textSize = textSize var totalWidth = 0.0 + var totalHeight: CGFloat = 54.0 totalWidth += textSize.width + + if let subtitleNode = self.subtitleNode { + let subtitleSize = subtitleNode.updateLayout(CGSize(width: constrainedSize.width * 0.75, height: constrainedSize.height)) + self.subtitleSize = subtitleSize + totalWidth = max(totalWidth, subtitleSize.width) + totalHeight += 14.0 + } + if let image = self.iconView?.image { if totalWidth > 0.0 { totalWidth += 11.0 @@ -130,7 +168,7 @@ final class ContextMenuActionNode: ASDisplayNode { totalWidth += 36.0 } - return CGSize(width: totalWidth, height: 54.0) + return CGSize(width: totalWidth, height: totalHeight) } else if let iconView = self.iconView, let image = iconView.image { return CGSize(width: image.size.width + 36.0, height: 54.0) } else { @@ -148,6 +186,9 @@ final class ContextMenuActionNode: ASDisplayNode { if let textSize = self.textSize { totalWidth += textSize.width } + if let subtitleSize = self.subtitleSize { + totalWidth = max(totalWidth, subtitleSize.width) + } if let image = self.iconView?.image { if totalWidth > 0.0 { totalWidth += 11.0 @@ -155,8 +196,18 @@ final class ContextMenuActionNode: ASDisplayNode { totalWidth += image.size.width } + var totalTextHeight: CGFloat = 0.0 + if let textSize = self.textSize { + totalTextHeight += textSize.height + } + if let subtitleSize = self.subtitleSize { + totalTextHeight += subtitleSize.height + } if let textNode = self.textNode, let textSize = self.textSize { - textNode.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - totalWidth) / 2.0), y: floor((self.bounds.size.height - textSize.height) / 2.0)), size: textSize) + textNode.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - totalWidth) / 2.0), y: floor((self.bounds.size.height - totalTextHeight) / 2.0)), size: textSize) + } + if let subtitleNode = self.subtitleNode, let subtitleSize = self.subtitleSize { + subtitleNode.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - totalWidth) / 2.0), y: floor((self.bounds.size.height - totalTextHeight) / 2.0) + totalTextHeight - subtitleSize.height), size: subtitleSize) } if let iconView = self.iconView, let image = iconView.image { let iconSize = image.size diff --git a/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuNode.swift b/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuNode.swift index 9e046f59877..581adfe1966 100644 --- a/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuNode.swift +++ b/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuNode.swift @@ -175,12 +175,11 @@ final class ContextMenuNode: ASDisplayNode { let separatorColor = self.isDark ? UIColor(rgb: 0x8c8e8e) : UIColor(rgb: 0xDCE3DC) - let height: CGFloat = 54.0 + var height: CGFloat = 54.0 - let pageLeftSize = self.pageLeftNode.update(color: self.isDark ? .white : .black, separatorColor: separatorColor, height: height) - let pageRightSize = self.pageRightNode.update(color: self.isDark ? .white : .black, separatorColor: separatorColor, height: height) - - let maxPageWidth = layout.size.width - 20.0 - pageLeftSize.width - pageRightSize.width + let handleWidth: CGFloat = 33.0 + + let maxPageWidth = layout.size.width - 20.0 - handleWidth * 2.0 var absoluteActionOffsetX: CGFloat = 0.0 var pages: [Page] = [] @@ -188,7 +187,8 @@ final class ContextMenuNode: ASDisplayNode { if i != 0 { absoluteActionOffsetX += UIScreenPixel } - let actionSize = self.actionNodes[i].measure(CGSize(width: layout.size.width, height: height)) + let actionSize = self.actionNodes[i].measure(CGSize(width: layout.size.width, height: 100.0)) + height = max(height, actionSize.height) if pages.isEmpty || (pages[pages.count - 1].width + actionSize.width) > maxPageWidth { pages.append(Page(range: i ..< (i + 1), width: actionSize.width, offsetX: absoluteActionOffsetX)) } else { @@ -212,6 +212,9 @@ final class ContextMenuNode: ASDisplayNode { separatorNode.isHidden = i == self.actionNodes.count - 1 } + let pageLeftSize = self.pageLeftNode.update(color: self.isDark ? .white : .black, separatorColor: separatorColor, height: height) + let pageRightSize = self.pageRightNode.update(color: self.isDark ? .white : .black, separatorColor: separatorColor, height: height) + self.pageCount = pages.count if !pages.isEmpty { diff --git a/submodules/TelegramUI/Components/ContextReferenceButtonComponent/Sources/ContextReferenceButtonComponent.swift b/submodules/TelegramUI/Components/ContextReferenceButtonComponent/Sources/ContextReferenceButtonComponent.swift index 08c88c2379d..f7bb1bd3447 100644 --- a/submodules/TelegramUI/Components/ContextReferenceButtonComponent/Sources/ContextReferenceButtonComponent.swift +++ b/submodules/TelegramUI/Components/ContextReferenceButtonComponent/Sources/ContextReferenceButtonComponent.swift @@ -82,7 +82,7 @@ public final class ContextReferenceButtonComponent: Component { self.component?.action(self, nil) } - public func update(component: ContextReferenceButtonComponent, availableSize: CGSize, transition: Transition) -> CGSize { + public func update(component: ContextReferenceButtonComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component let componentSize = self.componentView.update( @@ -118,7 +118,7 @@ public final class ContextReferenceButtonComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/TelegramUI/Components/DynamicCornerRadiusView/Sources/DynamicCornerRadiusView.swift b/submodules/TelegramUI/Components/DynamicCornerRadiusView/Sources/DynamicCornerRadiusView.swift index 531a3bb4a83..3227496f7db 100644 --- a/submodules/TelegramUI/Components/DynamicCornerRadiusView/Sources/DynamicCornerRadiusView.swift +++ b/submodules/TelegramUI/Components/DynamicCornerRadiusView/Sources/DynamicCornerRadiusView.swift @@ -67,7 +67,7 @@ open class DynamicCornerRadiusView: UIView { fatalError("init(coder:) has not been implemented") } - public func update(size: CGSize, corners: Corners, transition: Transition) { + public func update(size: CGSize, corners: Corners, transition: ComponentTransition) { let params = Params(size: size, corners: corners) if self.params == params { return @@ -76,13 +76,13 @@ open class DynamicCornerRadiusView: UIView { self.update(params: params, transition: transition) } - public func updateColor(color: UIColor, transition: Transition) { + public func updateColor(color: UIColor, transition: ComponentTransition) { if let shapeLayer = self.layer as? CAShapeLayer { transition.setShapeLayerFillColor(layer: shapeLayer, color: color) } } - private func update(params: Params, transition: Transition) { + private func update(params: Params, transition: ComponentTransition) { if let shapeLayer = self.layer as? CAShapeLayer { transition.setShapeLayerPath(layer: shapeLayer, path: generatePath(size: params.size, corners: params.corners)) } diff --git a/submodules/TelegramUI/Components/EmojiActionIconComponent/Sources/EmojiActionIconComponent.swift b/submodules/TelegramUI/Components/EmojiActionIconComponent/Sources/EmojiActionIconComponent.swift index 6bee0832add..e0194d41786 100644 --- a/submodules/TelegramUI/Components/EmojiActionIconComponent/Sources/EmojiActionIconComponent.swift +++ b/submodules/TelegramUI/Components/EmojiActionIconComponent/Sources/EmojiActionIconComponent.swift @@ -43,7 +43,7 @@ public final class EmojiActionIconComponent: Component { public final class View: UIView { private var icon: ComponentView? - func update(component: EmojiActionIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EmojiActionIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let size = CGSize(width: 24.0, height: 24.0) var iconSize = size @@ -109,7 +109,7 @@ public final class EmojiActionIconComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift index 158931012bb..b8a29122892 100644 --- a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift @@ -239,7 +239,7 @@ public final class EmojiStatusComponent: Component { } } - func update(component: EmojiStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EmojiStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let availableSize = component.size ?? availableSize self.state = state @@ -645,7 +645,7 @@ public final class EmojiStatusComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusPreviewScreen.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusPreviewScreen.swift index c9d0fcd28f6..48e767cecfb 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusPreviewScreen.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusPreviewScreen.swift @@ -57,7 +57,7 @@ private final class ContextMenuActionItem: Component, ContextMenuItemWithAction fatalError("init(coder:) has not been implemented") } - func update(component: ContextMenuActionItem, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ContextMenuActionItem, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let contextEnvironment = environment[EnvironmentType.self].value let sideInset: CGFloat = 16.0 @@ -87,7 +87,7 @@ private final class ContextMenuActionItem: Component, ContextMenuItemWithAction return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -263,14 +263,14 @@ private final class ContextMenuActionsComponent: Component { return self } - func update(component: ContextMenuActionsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ContextMenuActionsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let availableItemSize = availableSize var itemsSize = CGSize() var validIds = Set() - var currentItems: [(id: AnyHashable, itemFrame: CGRect, itemTransition: Transition)] = [] + var currentItems: [(id: AnyHashable, itemFrame: CGRect, itemTransition: ComponentTransition)] = [] for i in 0 ..< component.items.count { let item = component.items[i] validIds.insert(item.id) @@ -339,7 +339,7 @@ private final class ContextMenuActionsComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -419,7 +419,7 @@ private final class TimeSelectionControlComponent: Component { @objc private func datePickerUpdated() { } - func update(component: TimeSelectionControlComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: TimeSelectionControlComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.component?.theme !== component.theme { UILabel.setDateLabel(component.theme.list.itemPrimaryTextColor) @@ -521,7 +521,7 @@ private final class TimeSelectionControlComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -629,14 +629,14 @@ final class EmojiStatusPreviewScreenComponent: Component { switch self.currentState { case .menu: self.currentState = .timeSelection - self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.5, curve: .spring))) case .timeSelection: self.currentState = .menu - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } } - func update(component: EmojiStatusPreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EmojiStatusPreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -858,7 +858,7 @@ final class EmojiStatusPreviewScreenComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 5fca0d36fc5..9b437cf0967 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -67,7 +67,7 @@ public final class EmojiStatusSelectionComponent: Component { public let color: UIColor? public let hideTopPanel: Bool public let disableTopPanel: Bool - public let hideTopPanelUpdated: (Bool, Transition) -> Void + public let hideTopPanelUpdated: (Bool, ComponentTransition) -> Void public init( theme: PresentationTheme, @@ -79,7 +79,7 @@ public final class EmojiStatusSelectionComponent: Component { separatorColor: UIColor, hideTopPanel: Bool, disableTopPanel: Bool, - hideTopPanelUpdated: @escaping (Bool, Transition) -> Void + hideTopPanelUpdated: @escaping (Bool, ComponentTransition) -> Void ) { self.theme = theme self.strings = strings @@ -156,7 +156,7 @@ public final class EmojiStatusSelectionComponent: Component { deinit { } - func update(component: EmojiStatusSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EmojiStatusSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.backgroundColor = component.backgroundColor let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85) self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate) @@ -249,7 +249,7 @@ public final class EmojiStatusSelectionComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -736,7 +736,7 @@ public final class EmojiStatusSelectionController: ViewController { self.emojiSearchDisposable.dispose() } - private func refreshLayout(transition: Transition) { + private func refreshLayout(transition: ComponentTransition) { guard let layout = self.validLayout else { return } @@ -968,7 +968,7 @@ public final class EmojiStatusSelectionController: ViewController { }, fromBackground: fromBackground) } - func containerLayoutUpdated(layout: ContainerViewLayout, transition: Transition) { + func containerLayoutUpdated(layout: ContainerViewLayout, transition: ComponentTransition) { self.validLayout = layout var transition = transition @@ -1001,7 +1001,7 @@ public final class EmojiStatusSelectionController: ViewController { if let scheduledEmojiContentAnimationHint = self.scheduledEmojiContentAnimationHint { self.scheduledEmojiContentAnimationHint = nil let contentAnimation = scheduledEmojiContentAnimationHint - transition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) + transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) } var componentWidth = layout.size.width - sideInset * 2.0 @@ -1154,7 +1154,7 @@ public final class EmojiStatusSelectionController: ViewController { if let current = self.previewScreenView { previewScreenView = current } else { - previewScreenTransition = Transition(animation: .none) + previewScreenTransition = ComponentTransition(animation: .none) if let emojiView = self.componentHost.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View, let sourceLayer = emojiView.layerForItem(groupId: previewItem.groupId, item: previewItem.item) { previewScreenTransition = previewScreenTransition.withUserData(EmojiStatusPreviewScreenComponent.TransitionAnimation( transitionType: .animateIn(sourceLayer: sourceLayer) @@ -1493,7 +1493,7 @@ public final class EmojiStatusSelectionController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) } } diff --git a/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift index 348ce54803e..6a21882f2be 100644 --- a/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift +++ b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift @@ -390,7 +390,7 @@ public final class EmojiSuggestionsComponent: Component { //self.blurView.shadowPath = path } - func update(component: EmojiSuggestionsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EmojiSuggestionsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let height: CGFloat = 54.0 if self.component?.theme.backgroundColor != component.theme.backgroundColor { @@ -434,7 +434,7 @@ public final class EmojiSuggestionsComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 6613b4752b9..803ff111242 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -352,6 +352,44 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } } + public weak var mirrorLayer: CALayer? { + didSet { + if let mirrorLayer = self.mirrorLayer { + mirrorLayer.contents = self.contents + + var customColor = self.contentTintColor + if let file = self.file { + if file.isCustomTemplateEmoji { + customColor = self.dynamicColor + } + } + + if customColor != nil { + if self.layerTintColor == nil { + setLayerContentsMaskMode(mirrorLayer, true) + } + } else { + if self.layerTintColor != nil { + setLayerContentsMaskMode(mirrorLayer, false) + } + } + if let customColor { + ComponentTransition.immediate.setTintColor(layer: mirrorLayer, color: customColor) + } else { + self.layerTintColor = nil + } + } + } + } + + override public var contents: Any? { + didSet { + if let mirrorLayer = self.mirrorLayer { + mirrorLayer.contents = self.contents + } + } + } + public convenience init(context: AccountContext, userLocation: MediaResourceUserLocation, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool = false, placeholderColor: UIColor, pointSize: CGSize, dynamicColor: UIColor? = nil, loopCount: Int? = nil) { self.init( context: .account(context), @@ -395,9 +433,11 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { self.updateTopicInfo(topicInfo: (id, info)) case let .nameColors(colors): self.updateNameColors(colors: colors) - case .stars: - self.updateStars() - self.updateTintColor() + case let .stars(tinted): + self.updateStars(tinted: tinted) + if tinted { + self.updateTintColor() + } } } else if let file = file { self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad) @@ -442,7 +482,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { return nullAction } - public func updateTintColor(contentTintColor: UIColor?, dynamicColor: UIColor?, transition: Transition) { + public func updateTintColor(contentTintColor: UIColor?, dynamicColor: UIColor?, transition: ComponentTransition) { self._contentTintColor = contentTintColor self._dynamicColor = dynamicColor @@ -579,8 +619,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { self.contents = image?.cgImage } - private func updateStars() { - self.contents = starImage?.cgImage + private func updateStars(tinted: Bool) { + self.contents = tinted ? tintedStarImage?.cgImage : starImage?.cgImage } private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) { @@ -843,7 +883,7 @@ public final class CustomEmojiContainerView: UIView { } } -private let starImage: UIImage? = { +private let tintedStarImage: UIImage? = { generateImage(CGSize(width: 32.0, height: 32.0), contextGenerator: { size, context in context.clear(CGRect(origin: .zero, size: size)) @@ -852,3 +892,14 @@ private let starImage: UIImage? = { } })?.withRenderingMode(.alwaysTemplate) }() + + +private let starImage: UIImage? = { + generateImage(CGSize(width: 32.0, height: 32.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + if let image = UIImage(bundleImageName: "Premium/Stars/StarLarge"), let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 2.0, dy: 2.0), byTiling: false) + } + })?.withRenderingMode(.alwaysTemplate) +}() diff --git a/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift index 11a58b0a010..a9777433301 100644 --- a/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift @@ -90,7 +90,7 @@ public final class EmptyStateIndicatorComponent: Component { preconditionFailure() } - public func update(component: EmptyStateIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(component: EmptyStateIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.componentState = state @@ -266,7 +266,7 @@ public final class EmptyStateIndicatorComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift index 19ee0562aeb..78240287326 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift @@ -400,7 +400,7 @@ public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget { } func update( - transition: Transition, + transition: ComponentTransition, size: CGSize, badge: Badge?, blurredBadgeColor: UIColor, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 00c9b967c50..db64a3829f4 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -129,7 +129,7 @@ public protocol EmojiContentPeekBehavior: AnyObject { public protocol EmojiCustomContentView: UIView { var tintContainerView: UIView { get } - func update(theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, availableSize: CGSize, transition: Transition) -> CGSize + func update(theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, availableSize: CGSize, transition: ComponentTransition) -> CGSize } public final class EmojiPagerContentComponent: Component { @@ -238,7 +238,7 @@ public final class EmojiPagerContentComponent: Component { public let presentController: (ViewController) -> Void public let presentGlobalOverlayController: (ViewController) -> Void public let navigationController: () -> NavigationController? - public let requestUpdate: (Transition) -> Void + public let requestUpdate: (ComponentTransition) -> Void public let updateSearchQuery: (EmojiPagerContentComponent.SearchQuery?) -> Void public let updateScrollingToItemGroup: () -> Void public let externalCancel: (() -> Void)? @@ -268,7 +268,7 @@ public final class EmojiPagerContentComponent: Component { presentController: @escaping (ViewController) -> Void, presentGlobalOverlayController: @escaping (ViewController) -> Void, navigationController: @escaping () -> NavigationController?, - requestUpdate: @escaping (Transition) -> Void, + requestUpdate: @escaping (ComponentTransition) -> Void, updateSearchQuery: @escaping (SearchQuery?) -> Void, updateScrollingToItemGroup: @escaping () -> Void, externalCancel: (() -> Void)? = nil, @@ -1387,7 +1387,7 @@ public final class EmojiPagerContentComponent: Component { private var isUpdating: Bool = false private var pagerEnvironment: PagerComponentChildEnvironment? private var keyboardChildEnvironment: EntityKeyboardChildEnvironment? - private var activeItemUpdated: ActionSlot<(AnyHashable, AnyHashable?, Transition)>? + private var activeItemUpdated: ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>? private var itemLayout: ItemLayout? private var contextFocusItemKey: EmojiKeyboardItemLayer.Key? @@ -2779,7 +2779,7 @@ public final class EmojiPagerContentComponent: Component { itemLayer.cloneLayer = currentLongPressLayer itemLayer.isHidden = true - let transition = Transition(animation: .curve(duration: longPressDuration, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: longPressDuration, curve: .easeInOut)) transition.setScale(layer: currentLongPressLayer, scale: 1.85) } @@ -2806,13 +2806,13 @@ public final class EmojiPagerContentComponent: Component { self.longPressItem = nil if let itemLayer = self.visibleItemLayers[itemKey] { - let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) transition.setScale(layer: itemLayer, scale: 1.0) if let currentLongPressLayer = self.currentLongPressLayer { self.currentLongPressLayer = nil - let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) transition.setScale(layer: currentLongPressLayer, scale: 1.0, completion: { [weak itemLayer, weak currentLongPressLayer] _ in itemLayer?.isHidden = false currentLongPressLayer?.removeFromSuperlayer() @@ -2841,13 +2841,13 @@ public final class EmojiPagerContentComponent: Component { } } else { if let itemLayer = self.visibleItemLayers[itemKey] { - let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) transition.setScale(layer: itemLayer, scale: 1.0) if let currentLongPressLayer = self.currentLongPressLayer { self.currentLongPressLayer = nil - let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) transition.setScale(layer: currentLongPressLayer, scale: 1.0, completion: { [weak itemLayer, weak currentLongPressLayer] _ in itemLayer?.isHidden = false currentLongPressLayer?.removeFromSuperlayer() @@ -2856,7 +2856,7 @@ public final class EmojiPagerContentComponent: Component { } else if let currentLongPressLayer = self.currentLongPressLayer { self.currentLongPressLayer = nil - let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) transition.setScale(layer: currentLongPressLayer, scale: 1.0, completion: { [weak currentLongPressLayer] _ in currentLongPressLayer?.removeFromSuperlayer() }) @@ -2970,7 +2970,7 @@ public final class EmojiPagerContentComponent: Component { self.component?.inputInteractionHolder.inputInteraction?.scrollingStickersGridPromise.set(false) } - private func updateScrollingOffset(isReset: Bool, transition: Transition) { + private func updateScrollingOffset(isReset: Bool, transition: ComponentTransition) { guard let component = self.component else { return } @@ -3014,7 +3014,7 @@ public final class EmojiPagerContentComponent: Component { } private func snapScrollingOffsetToInsets() { - let transition = Transition(animation: .curve(duration: 0.4, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)) var currentBounds = self.scrollView.bounds currentBounds.origin.y = self.snappedContentOffset(proposedOffset: currentBounds.minY) @@ -3023,7 +3023,7 @@ public final class EmojiPagerContentComponent: Component { self.updateScrollingOffset(isReset: false, transition: transition) } - private func updateVisibleItems(transition: Transition, attemptSynchronousLoads: Bool, previousItemPositions: [VisualItemKey: CGPoint]?, previousAbsoluteItemPositions: [VisualItemKey: CGPoint]? = nil, updatedItemPositions: [VisualItemKey: CGPoint]?, hintDisappearingGroupFrame: (groupId: AnyHashable, frame: CGRect)? = nil) { + private func updateVisibleItems(transition: ComponentTransition, attemptSynchronousLoads: Bool, previousItemPositions: [VisualItemKey: CGPoint]?, previousAbsoluteItemPositions: [VisualItemKey: CGPoint]? = nil, updatedItemPositions: [VisualItemKey: CGPoint]?, hintDisappearingGroupFrame: (groupId: AnyHashable, frame: CGRect)? = nil) { guard let component = self.component, let pagerEnvironment = self.pagerEnvironment, let keyboardChildEnvironment = self.keyboardChildEnvironment, let itemLayout = self.itemLayout else { return } @@ -3971,10 +3971,10 @@ public final class EmojiPagerContentComponent: Component { private func expandGroup(groupId: AnyHashable) { self.expandedGroupIds.insert(groupId) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(ContentAnimation(type: .groupExpanded(id: groupId)))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(ContentAnimation(type: .groupExpanded(id: groupId)))) } - public func pagerUpdateBackground(backgroundFrame: CGRect, topPanelHeight: CGFloat, transition: Transition) { + public func pagerUpdateBackground(backgroundFrame: CGRect, topPanelHeight: CGFloat, transition: ComponentTransition) { guard let component = self.component, let keyboardChildEnvironment = self.keyboardChildEnvironment, let pagerEnvironment = self.pagerEnvironment else { return } @@ -4055,7 +4055,7 @@ public final class EmojiPagerContentComponent: Component { } } - func update(component: EmojiPagerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EmojiPagerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -4580,16 +4580,16 @@ public final class EmojiPagerContentComponent: Component { if !isFirstResponder { strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate( - Transition(animation: .curve(duration: 0.4, curve: .spring))) + ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } else { DispatchQueue.main.async { self?.component?.inputInteractionHolder.inputInteraction?.requestUpdate( - Transition(animation: .curve(duration: 0.4, curve: .spring))) + ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } } if !strongSelf.isUpdating { - strongSelf.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + strongSelf.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } } }, updateQuery: { [weak self] query in @@ -4789,7 +4789,7 @@ public final class EmojiPagerContentComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift index e8fb0bd8c9a..18208353561 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift @@ -166,7 +166,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode self.onCancel?() } else { self.itemGroups.removeAll(where: { $0.groupId == groupId }) - self.update(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(EmojiPagerContentComponent.ContentAnimation(type: .groupRemoved(id: groupId)))) + self.update(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(EmojiPagerContentComponent.ContentAnimation(type: .groupRemoved(id: groupId)))) } } }, @@ -441,17 +441,17 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode self.dataDisposable?.dispose() } - private func update(transition: Transition) { + private func update(transition: ComponentTransition) { if let params = self.params { self.update(size: params.size, leftInset: params.leftInset, rightInset: params.rightInset, bottomInset: params.bottomInset, inputHeight: params.inputHeight, deviceMetrics: params.deviceMetrics, transition: transition) } } public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) { - self.update(size: size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: Transition(transition)) + self.update(size: size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: ComponentTransition(transition)) } - private func update(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: Transition) { + private func update(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ComponentTransition) { self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor let params = Params(size: size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchHeaderView.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchHeaderView.swift index 272fb06789a..5eb70b2d572 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchHeaderView.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchHeaderView.swift @@ -340,7 +340,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { self.updateQuery(.text(value: text, language: inputLanguage)) } - private func update(transition: Transition) { + private func update(transition: ComponentTransition) { guard let params = self.params else { return } @@ -348,7 +348,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { self.update(context: params.context, theme: params.theme, forceNeedsVibrancy: params.forceNeedsVibrancy, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, searchCategories: params.searchCategories, searchState: params.searchState, transition: transition) } - public func update(context: AccountContext, theme: PresentationTheme, forceNeedsVibrancy: Bool, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, searchCategories: EmojiSearchCategories?, searchState: EmojiPagerContentComponent.SearchState, transition: Transition) { + public func update(context: AccountContext, theme: PresentationTheme, forceNeedsVibrancy: Bool, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, searchCategories: EmojiSearchCategories?, searchState: EmojiPagerContentComponent.SearchState, transition: ComponentTransition) { let textInputState: EmojiSearchSearchBarComponent.TextInputState if let textField = self.textField { textInputState = .active(hasText: !(textField.text ?? "").isEmpty) @@ -517,7 +517,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { if shouldChangeActivation { if let term { - self.update(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.update(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) self.updateQuery(.category(value: term)) self.activated(false) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift index 9b9b67c1ea1..3c53dd5497e 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift @@ -383,7 +383,7 @@ final class EmojiSearchSearchBarComponent: Component { } } } else { - let transition = Transition(animation: .curve(duration: 0.4, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)) transition.setBoundsOrigin(view: self.scrollView, origin: CGPoint()) self.updateScrolling(transition: transition, fromScrolling: false) //self.scrollView.setContentOffset(CGPoint(), animated: true) @@ -402,7 +402,7 @@ final class EmojiSearchSearchBarComponent: Component { if self.selectedItem != nil { self.selectedItem = nil - let transition = Transition(animation: .curve(duration: 0.4, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)) transition.setBoundsOrigin(view: self.scrollView, origin: CGPoint()) self.updateScrolling(transition: transition, fromScrolling: false) @@ -438,7 +438,7 @@ final class EmojiSearchSearchBarComponent: Component { return (itemLayout.itemStartX - itemLayout.textSpacing) + visibleBounds.minX } - private func updateScrolling(transition: Transition, fromScrolling: Bool) { + private func updateScrolling(transition: ComponentTransition, fromScrolling: Bool) { guard let component = self.component, let itemLayout = self.itemLayout else { return } @@ -600,8 +600,8 @@ final class EmojiSearchSearchBarComponent: Component { self.selectedItemBackground.opacity = 1.0 self.selectedItemTintBackground.opacity = 1.0 - Transition.immediate.setScale(layer: self.selectedItemBackground, scale: 1.0) - Transition.immediate.setScale(layer: self.selectedItemTintBackground, scale: 1.0) + ComponentTransition.immediate.setScale(layer: self.selectedItemBackground, scale: 1.0) + ComponentTransition.immediate.setScale(layer: self.selectedItemTintBackground, scale: 1.0) if !transition.animation.isImmediate { self.selectedItemBackground.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -616,8 +616,8 @@ final class EmojiSearchSearchBarComponent: Component { transition.setPosition(layer: self.selectedItemTintBackground, position: selectionFrame.center) if case let .curve(duration, _) = transition.animation { - Transition.immediate.setScale(layer: self.selectedItemBackground, scale: 1.0) - Transition.immediate.setScale(layer: self.selectedItemTintBackground, scale: 1.0) + ComponentTransition.immediate.setScale(layer: self.selectedItemBackground, scale: 1.0) + ComponentTransition.immediate.setScale(layer: self.selectedItemTintBackground, scale: 1.0) self.selectedItemBackground.animateKeyframes(values: [1.0 as NSNumber, 0.75 as NSNumber, 1.0 as NSNumber], duration: duration, keyPath: "transform.scale") self.selectedItemTintBackground.animateKeyframes(values: [1.0 as NSNumber, 0.75 as NSNumber, 1.0 as NSNumber], duration: duration, keyPath: "transform.scale") @@ -644,7 +644,7 @@ final class EmojiSearchSearchBarComponent: Component { transition.setBounds(view: self.tintTextContainerView, bounds: CGRect(origin: CGPoint(x: textOffset, y: 0.0), size: scrollBounds.size)) } - func update(component: EmojiSearchSearchBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EmojiSearchSearchBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.componentState = state @@ -731,7 +731,7 @@ final class EmojiSearchSearchBarComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift index 4b498d229b0..6d4e8134271 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift @@ -430,7 +430,7 @@ final class EmojiSearchStatusComponent: Component { } } - func update(component: EmojiSearchStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EmojiSearchStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let displaySize = CGSize(width: availableSize.width * UIScreenScale, height: availableSize.height * UIScreenScale) @@ -791,7 +791,7 @@ final class EmojiSearchStatusComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmptySearchResultsView.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmptySearchResultsView.swift index 466c430e8da..b7d109e7410 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmptySearchResultsView.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmptySearchResultsView.swift @@ -33,7 +33,7 @@ final class EmptySearchResultsView: UIView { fatalError("init(coder:) has not been implemented") } - func update(context: AccountContext, theme: PresentationTheme, useOpaqueTheme: Bool, text: String, file: TelegramMediaFile?, size: CGSize, searchInitiallyHidden: Bool, transition: Transition) { + func update(context: AccountContext, theme: PresentationTheme, useOpaqueTheme: Bool, text: String, file: TelegramMediaFile?, size: CGSize, searchInitiallyHidden: Bool, transition: ComponentTransition) { let titleColor: UIColor if useOpaqueTheme { titleColor = theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 803c722538d..69cecf479e9 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -23,13 +23,13 @@ public final class EntityKeyboardChildEnvironment: Equatable { public let theme: PresentationTheme public let strings: PresentationStrings public let isContentInFocus: Bool - public let getContentActiveItemUpdated: (AnyHashable) -> ActionSlot<(AnyHashable, AnyHashable?, Transition)>? + public let getContentActiveItemUpdated: (AnyHashable) -> ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>? public init( theme: PresentationTheme, strings: PresentationStrings, isContentInFocus: Bool, - getContentActiveItemUpdated: @escaping (AnyHashable) -> ActionSlot<(AnyHashable, AnyHashable?, Transition)>? + getContentActiveItemUpdated: @escaping (AnyHashable) -> ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>? ) { self.theme = theme self.strings = strings @@ -119,10 +119,10 @@ public final class EntityKeyboardComponent: Component { public let externalTopPanelContainer: PagerExternalTopPanelContainer? public let externalBottomPanelContainer: PagerExternalTopPanelContainer? public let displayTopPanelBackground: DisplayTopPanelBackground - public let topPanelExtensionUpdated: (CGFloat, Transition) -> Void - public let topPanelScrollingOffset: (CGFloat, Transition) -> Void - public let hideInputUpdated: (Bool, Bool, Transition) -> Void - public let hideTopPanelUpdated: (Bool, Transition) -> Void + public let topPanelExtensionUpdated: (CGFloat, ComponentTransition) -> Void + public let topPanelScrollingOffset: (CGFloat, ComponentTransition) -> Void + public let hideInputUpdated: (Bool, Bool, ComponentTransition) -> Void + public let hideTopPanelUpdated: (Bool, ComponentTransition) -> Void public let switchToTextInput: () -> Void public let switchToGifSubject: (GifPagerContentComponent.Subject) -> Void public let reorderItems: (ReorderCategory, [EntityKeyboardTopPanelComponent.Item]) -> Void @@ -157,10 +157,10 @@ public final class EntityKeyboardComponent: Component { externalTopPanelContainer: PagerExternalTopPanelContainer?, externalBottomPanelContainer: PagerExternalTopPanelContainer?, displayTopPanelBackground: DisplayTopPanelBackground, - topPanelExtensionUpdated: @escaping (CGFloat, Transition) -> Void, - topPanelScrollingOffset: @escaping (CGFloat, Transition) -> Void, - hideInputUpdated: @escaping (Bool, Bool, Transition) -> Void, - hideTopPanelUpdated: @escaping (Bool, Transition) -> Void, + topPanelExtensionUpdated: @escaping (CGFloat, ComponentTransition) -> Void, + topPanelScrollingOffset: @escaping (CGFloat, ComponentTransition) -> Void, + hideInputUpdated: @escaping (Bool, Bool, ComponentTransition) -> Void, + hideTopPanelUpdated: @escaping (Bool, ComponentTransition) -> Void, switchToTextInput: @escaping () -> Void, switchToGifSubject: @escaping (GifPagerContentComponent.Subject) -> Void, reorderItems: @escaping (ReorderCategory, [EntityKeyboardTopPanelComponent.Item]) -> Void, @@ -324,7 +324,7 @@ public final class EntityKeyboardComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: EntityKeyboardComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EntityKeyboardComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.state = state var contents: [AnyComponentWithIdentity<(EntityKeyboardChildEnvironment, PagerComponentChildEnvironment)>] = [] @@ -333,9 +333,9 @@ public final class EntityKeyboardComponent: Component { var contentAccessoryLeftButtons: [AnyComponentWithIdentity] = [] var contentAccessoryRightButtons: [AnyComponentWithIdentity] = [] - let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>() - let stickersContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>() - let masksContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>() + let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>() + let stickersContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>() + let masksContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>() if transition.userData(MarkInputCollapsed.self) != nil { self.searchComponent = nil @@ -583,7 +583,7 @@ public final class EntityKeyboardComponent: Component { let deleteBackwards = component.emojiContent?.inputInteractionHolder.inputInteraction?.deleteBackwards - let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>() + let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>() if let emojiContent = component.emojiContent { contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(emojiContent))) var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = [] @@ -871,7 +871,7 @@ public final class EntityKeyboardComponent: Component { return availableSize } - private func topPanelExtensionUpdated(height: CGFloat, transition: Transition) { + private func topPanelExtensionUpdated(height: CGFloat, transition: ComponentTransition) { guard let component = self.component else { return } @@ -883,7 +883,7 @@ public final class EntityKeyboardComponent: Component { } } - private func topPanelScrollingOffset(offset: CGFloat, transition: Transition) { + private func topPanelScrollingOffset(offset: CGFloat, transition: ComponentTransition) { guard let component = self.component else { return } @@ -895,7 +895,7 @@ public final class EntityKeyboardComponent: Component { } } - private func isTopPanelExpandedUpdated(isExpanded: Bool, transition: Transition) { + private func isTopPanelExpandedUpdated(isExpanded: Bool, transition: ComponentTransition) { if self.isTopPanelExpanded != isExpanded { self.isTopPanelExpanded = isExpanded } @@ -907,7 +907,7 @@ public final class EntityKeyboardComponent: Component { component.hideInputUpdated(self.isTopPanelExpanded, false, transition) } - private func isTopPanelHiddenUpdated(isTopPanelHidden: Bool, transition: Transition) { + private func isTopPanelHiddenUpdated(isTopPanelHidden: Bool, transition: ComponentTransition) { if self.isTopPanelHidden != isTopPanelHidden { self.isTopPanelHidden = isTopPanelHidden } @@ -958,7 +958,7 @@ public final class EntityKeyboardComponent: Component { ) } - component.hideInputUpdated(true, true, Transition(animation: .curve(duration: 0.3, curve: .spring))) + component.hideInputUpdated(true, true, ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } } @@ -983,7 +983,7 @@ public final class EntityKeyboardComponent: Component { } ) } - component.hideInputUpdated(true, true, Transition(animation: .curve(duration: 0.3, curve: .spring))) + component.hideInputUpdated(true, true, ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } private func closeSearch() { @@ -994,8 +994,8 @@ public final class EntityKeyboardComponent: Component { return } self.searchComponent = nil - //self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) - component.hideInputUpdated(false, false, Transition(animation: .curve(duration: 0.4, curve: .spring))) + //self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) + component.hideInputUpdated(false, false, ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } public func scrollToItemGroup(contentId: String, groupId: AnyHashable, subgroupId: Int32?, animated: Bool = true) { @@ -1023,7 +1023,7 @@ public final class EntityKeyboardComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift index 369dfc812d4..73390b8127a 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift @@ -65,7 +65,7 @@ private final class BottomPanelIconComponent: Component { } } - func update(component: BottomPanelIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BottomPanelIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.component?.title != component.title { let text = NSAttributedString(string: component.title, font: Font.medium(15.0), textColor: .white) let textBounds = text.boundingRect(with: CGSize(width: 120.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) @@ -106,7 +106,7 @@ private final class BottomPanelIconComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -184,7 +184,7 @@ final class EntityKeyboardBottomPanelComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: EntityKeyboardBottomPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EntityKeyboardBottomPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.component?.theme !== component.theme { self.separatorView.backgroundColor = component.theme.chat.inputMediaPanel.panelSeparatorColor self.backgroundView.updateColor(color: component.theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(1.0), transition: .immediate) @@ -309,7 +309,7 @@ final class EntityKeyboardBottomPanelComponent: Component { } var validIconIds: [AnyHashable] = [] - var iconInfos: [AnyHashable: (size: CGSize, transition: Transition)] = [:] + var iconInfos: [AnyHashable: (size: CGSize, transition: ComponentTransition)] = [:] var iconTotalSize = CGSize() let iconSpacing: CGFloat = 4.0 @@ -411,7 +411,7 @@ final class EntityKeyboardBottomPanelComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift index 0d65cbf0a25..a8e4fae5082 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift @@ -9,13 +9,13 @@ import Postbox public final class EntityKeyboardTopContainerPanelEnvironment: Equatable { let isContentInFocus: Bool - let visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)> - let isExpandedUpdated: (Bool, Transition) -> Void + let visibilityFractionUpdated: ActionSlot<(CGFloat, ComponentTransition)> + let isExpandedUpdated: (Bool, ComponentTransition) -> Void init( isContentInFocus: Bool, - visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)>, - isExpandedUpdated: @escaping (Bool, Transition) -> Void + visibilityFractionUpdated: ActionSlot<(CGFloat, ComponentTransition)>, + isExpandedUpdated: @escaping (Bool, ComponentTransition) -> Void ) { self.isContentInFocus = isContentInFocus self.visibilityFractionUpdated = visibilityFractionUpdated @@ -66,7 +66,7 @@ final class EntityKeyboardTopContainerPanelComponent: Component { private final class PanelView { let view = ComponentHostView() - let visibilityFractionUpdated = ActionSlot<(CGFloat, Transition)>() + let visibilityFractionUpdated = ActionSlot<(CGFloat, ComponentTransition)>() var isExpanded: Bool = false } @@ -93,7 +93,7 @@ final class EntityKeyboardTopContainerPanelComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: EntityKeyboardTopContainerPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EntityKeyboardTopContainerPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let intrinsicHeight: CGFloat = 34.0 let height = intrinsicHeight @@ -245,7 +245,7 @@ final class EntityKeyboardTopContainerPanelComponent: Component { return CGSize(width: availableSize.width, height: height) } - private func updateVisibilityFraction(value: CGFloat, transition: Transition) { + private func updateVisibilityFraction(value: CGFloat, transition: ComponentTransition) { if self.visibilityFraction == value { return } @@ -257,7 +257,7 @@ final class EntityKeyboardTopContainerPanelComponent: Component { } } - private func panelIsExpandedUpdated(id: AnyHashable, isExpanded: Bool, transition: Transition) { + private func panelIsExpandedUpdated(id: AnyHashable, isExpanded: Bool, transition: ComponentTransition) { guard let panelView = self.panelViews[id] else { return } @@ -306,7 +306,7 @@ final class EntityKeyboardTopContainerPanelComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index 236588fff22..28dada2af5e 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -106,7 +106,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { } } - func update(component: EntityKeyboardAnimationTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EntityKeyboardAnimationTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let itemEnvironment = environment[EntityKeyboardTopPanelItemEnvironment.self].value @@ -262,7 +262,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -347,7 +347,7 @@ final class EntityKeyboardIconTopPanelComponent: Component { } } - func update(component: EntityKeyboardIconTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EntityKeyboardIconTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let itemEnvironment = environment[EntityKeyboardTopPanelItemEnvironment.self].value if self.component?.icon != component.icon { @@ -471,7 +471,7 @@ final class EntityKeyboardIconTopPanelComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -541,7 +541,7 @@ final class EntityKeyboardAvatarTopPanelComponent: Component { } } - func update(component: EntityKeyboardAvatarTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EntityKeyboardAvatarTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let itemEnvironment = environment[EntityKeyboardTopPanelItemEnvironment.self].value self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer) @@ -602,7 +602,7 @@ final class EntityKeyboardAvatarTopPanelComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -746,7 +746,7 @@ final class EntityKeyboardStaticStickersPanelComponent: Component { self.updateVisibleItems(transition: .immediate, animateAppearingItems: true) } - private func updateVisibleItems(transition: Transition, animateAppearingItems: Bool) { + private func updateVisibleItems(transition: ComponentTransition, animateAppearingItems: Bool) { guard let component = self.component, let itemEnvironment = self.itemEnvironment, let itemLayout = self.itemLayout else { return } @@ -854,7 +854,7 @@ final class EntityKeyboardStaticStickersPanelComponent: Component { } } - func update(component: EntityKeyboardStaticStickersPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EntityKeyboardStaticStickersPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let itemEnvironment = environment[EntityKeyboardTopPanelItemEnvironment.self].value var scrollToItem: AnyHashable? @@ -943,7 +943,7 @@ final class EntityKeyboardStaticStickersPanelComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -1210,7 +1210,7 @@ public final class EntityKeyboardTopPanelComponent: Component { let containerSideInset: CGFloat let defaultActiveItemId: AnyHashable? let forceActiveItemId: AnyHashable? - let activeContentItemIdUpdated: ActionSlot<(AnyHashable, AnyHashable?, Transition)> + let activeContentItemIdUpdated: ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)> let activeContentItemMapping: [AnyHashable: AnyHashable] let reorderItems: ([Item]) -> Void @@ -1222,7 +1222,7 @@ public final class EntityKeyboardTopPanelComponent: Component { containerSideInset: CGFloat, defaultActiveItemId: AnyHashable? = nil, forceActiveItemId: AnyHashable? = nil, - activeContentItemIdUpdated: ActionSlot<(AnyHashable, AnyHashable?, Transition)>, + activeContentItemIdUpdated: ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>, activeContentItemMapping: [AnyHashable: AnyHashable] = [:], reorderItems: @escaping ([Item]) -> Void ) { @@ -1637,7 +1637,7 @@ public final class EntityKeyboardTopPanelComponent: Component { guard let environment = strongSelf.environment else { return } - environment.isExpandedUpdated(false, Transition(animation: .curve(duration: 0.3, curve: .spring))) + environment.isExpandedUpdated(false, ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) }, queue: .mainQueue()) self.draggingStoppedTimer?.start() } @@ -1651,7 +1651,7 @@ public final class EntityKeyboardTopPanelComponent: Component { guard let environment = self.environment else { return } - environment.isExpandedUpdated(true, Transition(animation: .curve(duration: 0.3, curve: .spring))) + environment.isExpandedUpdated(true, ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } } } @@ -1734,7 +1734,7 @@ public final class EntityKeyboardTopPanelComponent: Component { if self.didReorderItems { self.component?.reorderItems(self.items) } - //self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + //self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } private func updateReordering(offset: CGFloat) { @@ -1760,7 +1760,7 @@ public final class EntityKeyboardTopPanelComponent: Component { self.reorderingHapticFeedback.tap() } self.didReorderItems = true - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } break } @@ -1795,7 +1795,7 @@ public final class EntityKeyboardTopPanelComponent: Component { } } - private func updateVisibleItems(attemptSynchronousLoads: Bool, transition: Transition) { + private func updateVisibleItems(attemptSynchronousLoads: Bool, transition: ComponentTransition) { guard let itemLayout = self.itemLayout else { return } @@ -1865,7 +1865,7 @@ public final class EntityKeyboardTopPanelComponent: Component { } } - func update(component: EntityKeyboardTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EntityKeyboardTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.component?.theme !== component.theme || self.component?.customTintColor != component.customTintColor { if let customTintColor = component.customTintColor { self.highlightedIconBackgroundView.backgroundColor = customTintColor.withAlphaComponent(0.1) @@ -2115,7 +2115,7 @@ public final class EntityKeyboardTopPanelComponent: Component { return CGSize(width: availableSize.width, height: height) } - private func visibilityFractionUpdated(value: CGFloat, transition: Transition) { + private func visibilityFractionUpdated(value: CGFloat, transition: ComponentTransition) { if self.visibilityFraction == value { return } @@ -2133,7 +2133,7 @@ public final class EntityKeyboardTopPanelComponent: Component { } } - private func activeContentItemIdUpdated(itemId: AnyHashable, subcontentItemId: AnyHashable?, transition: Transition) { + private func activeContentItemIdUpdated(itemId: AnyHashable, subcontentItemId: AnyHashable?, transition: ComponentTransition) { guard let component = self.component, let itemLayout = self.itemLayout else { return } @@ -2145,7 +2145,7 @@ public final class EntityKeyboardTopPanelComponent: Component { let _ = component let _ = itemLayout - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) if let component = self.component, let itemLayout = self.itemLayout { for i in 0 ..< component.items.count { @@ -2199,7 +2199,7 @@ public final class EntityKeyboardTopPanelComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntitySearchContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntitySearchContentComponent.swift index 5068066d1cd..05ba06d5b9e 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntitySearchContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntitySearchContentComponent.swift @@ -137,7 +137,7 @@ final class EntitySearchContentComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: EntitySearchContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EntitySearchContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let containerNode: EntitySearchContainerNode? if let current = self.containerNode { containerNode = current @@ -174,7 +174,7 @@ final class EntitySearchContentComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index c4943a46a79..32696398039 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -762,7 +762,7 @@ public final class GifPagerContentComponent: Component { self.snapScrollingOffsetToInsets() } - private func updateScrollingOffset(transition: Transition) { + private func updateScrollingOffset(transition: ComponentTransition) { let isInteracting = self.scrollView.isDragging || self.scrollView.isDecelerating if let previousScrollingOffsetValue = self.previousScrollingOffset { let currentBounds = self.scrollView.bounds @@ -808,7 +808,7 @@ public final class GifPagerContentComponent: Component { } private func snapScrollingOffsetToInsets() { - let transition = Transition(animation: .curve(duration: 0.4, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)) var currentBounds = self.scrollView.bounds currentBounds.origin.y = self.snappedContentOffset(proposedOffset: currentBounds.minY) @@ -818,7 +818,7 @@ public final class GifPagerContentComponent: Component { self.updateVisibleItems(attemptSynchronousLoads: false, transition: transition, fromScrolling: true) } - private func updateVisibleItems(attemptSynchronousLoads: Bool, transition: Transition, fromScrolling: Bool) { + private func updateVisibleItems(attemptSynchronousLoads: Bool, transition: ComponentTransition, fromScrolling: Bool) { guard let component = self.component, let itemLayout = self.itemLayout else { return } @@ -853,7 +853,7 @@ public final class GifPagerContentComponent: Component { let itemFrame = itemLayout.frame(at: index).offsetBy(dx: 0.0, dy: searchInset) - var itemTransition: Transition = transition + var itemTransition: ComponentTransition = transition var updateItemLayerPlaceholder = false let itemLayer: ItemLayer @@ -972,7 +972,7 @@ public final class GifPagerContentComponent: Component { } } - public func pagerUpdateBackground(backgroundFrame: CGRect, topPanelHeight: CGFloat, transition: Transition) { + public func pagerUpdateBackground(backgroundFrame: CGRect, topPanelHeight: CGFloat, transition: ComponentTransition) { guard let theme = self.theme else { return } @@ -1009,7 +1009,7 @@ public final class GifPagerContentComponent: Component { } } - func update(component: GifPagerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: GifPagerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { var contentReset = false if let previousComponent = self.component, previousComponent.subject != component.subject { contentReset = true @@ -1129,7 +1129,7 @@ public final class GifPagerContentComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupEmbeddedView.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupEmbeddedView.swift index 4721e8897f2..9fba3ea8fdb 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupEmbeddedView.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupEmbeddedView.swift @@ -104,7 +104,7 @@ final class GroupEmbeddedView: UIScrollView, UIScrollViewDelegate, PagerExpandab } } - private func updateVisibleItems(transition: Transition, attemptSynchronousLoad: Bool) { + private func updateVisibleItems(transition: ComponentTransition, attemptSynchronousLoad: Bool) { guard let context = self.context, let theme = self.theme, let itemLayout = self.itemLayout, let items = self.items, let cache = self.cache, let renderer = self.renderer else { return } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/PremiumBadgeView.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/PremiumBadgeView.swift index 6e0a0497d23..d2d5c1f6c77 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/PremiumBadgeView.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/PremiumBadgeView.swift @@ -46,7 +46,7 @@ final class PremiumBadgeView: UIView { fatalError("init(coder:) has not been implemented") } - func update(transition: Transition, badge: EmojiKeyboardItemLayer.Badge, backgroundColor: UIColor, size: CGSize) { + func update(transition: ComponentTransition, badge: EmojiKeyboardItemLayer.Badge, backgroundColor: UIColor, size: CGSize) { if self.badge != badge { self.badge = badge diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/WarpView.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/WarpView.swift index 7ccea1a793e..592ff508476 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/WarpView.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/WarpView.swift @@ -27,7 +27,7 @@ final class WarpView: UIView { fatalError("init(coder:) has not been implemented") } - func update(containerSize: CGSize, rect: CGRect, transition: Transition) { + func update(containerSize: CGSize, rect: CGRect, transition: ComponentTransition) { transition.setFrame(view: self.cloneView.view, frame: CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: CGSize(width: containerSize.width, height: containerSize.height))) } } @@ -68,7 +68,7 @@ final class WarpView: UIView { fatalError("init(coder:) has not been implemented") } - func update(size: CGSize, topInset: CGFloat, warpHeight: CGFloat, theme: PresentationTheme, transition: Transition) { + func update(size: CGSize, topInset: CGFloat, warpHeight: CGFloat, theme: PresentationTheme, transition: ComponentTransition) { transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: size)) let allItemsHeight = warpHeight * 0.5 diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index e7d812db0e9..ffb5fa4e49b 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -159,7 +159,7 @@ private final class TitleFieldComponent: Component { return true } - func update(component: TitleFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: TitleFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.textField.textColor = component.textColor self.textField.text = component.text self.textField.font = Font.regular(17.0) @@ -237,7 +237,7 @@ private final class TitleFieldComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -322,7 +322,7 @@ private final class TopicIconSelectionComponent: Component { deinit { } - func update(component: TopicIconSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: TopicIconSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.backgroundColor = component.backgroundColor let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85) self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate) @@ -402,7 +402,7 @@ private final class TopicIconSelectionComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift index 78b09b15973..677c0472156 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift @@ -7,6 +7,8 @@ import AppBundle import ComponentFlow import TextFormat import MessageInlineBlockBackgroundView +import InvisibleInkDustNode +import EmojiTextAttachmentView private let defaultFont = UIFont.systemFont(ofSize: 15.0) @@ -33,12 +35,19 @@ private func generateBlockMaskImage() -> UIImage { let colorSpace = CGColorSpaceCreateDeviceRGB() var locations: [CGFloat] = [0.0, 0.5, 1.0] - let colors: [CGColor] = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.cgColor] + var colors: [CGColor] = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.cgColor] - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + var gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! context.setBlendMode(.copy) context.drawRadialGradient(gradient, startCenter: CGPoint(x: size.width - 20.0, y: size.height), startRadius: 0.0, endCenter: CGPoint(x: size.width - 20.0, y: size.height), endRadius: 34.0, options: CGGradientDrawingOptions()) + + locations = [0.0, 0.4, 1.0] + colors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.cgColor] + gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.setBlendMode(.destinationIn) + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: size.height), end: CGPoint(x: 0.0, y: size.height - 8.0), options: CGGradientDrawingOptions()) })!.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: size.height - 1.0, right: size.width - 1.0), resizingMode: .stretch) } @@ -94,10 +103,13 @@ private final class InteractiveTextNodeAttachment { private final class InteractiveTextNodeLine { let line: CTLine + let constrainedWidth: CGFloat var frame: CGRect + let intrinsicWidth: CGFloat let ascent: CGFloat let descent: CGFloat let range: NSRange? + let isTruncated: Bool let isRTL: Bool var strikethroughs: [InteractiveTextNodeStrikethrough] var spoilers: [InteractiveTextNodeSpoiler] @@ -106,12 +118,15 @@ private final class InteractiveTextNodeLine { var attachments: [InteractiveTextNodeAttachment] let additionalTrailingLine: (CTLine, Double)? - init(line: CTLine, frame: CGRect, ascent: CGFloat, descent: CGFloat, range: NSRange?, isRTL: Bool, strikethroughs: [InteractiveTextNodeStrikethrough], spoilers: [InteractiveTextNodeSpoiler], spoilerWords: [InteractiveTextNodeSpoiler], embeddedItems: [InteractiveTextNodeEmbeddedItem], attachments: [InteractiveTextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) { + init(line: CTLine, constrainedWidth: CGFloat, frame: CGRect, intrinsicWidth: CGFloat, ascent: CGFloat, descent: CGFloat, range: NSRange?, isTruncated: Bool, isRTL: Bool, strikethroughs: [InteractiveTextNodeStrikethrough], spoilers: [InteractiveTextNodeSpoiler], spoilerWords: [InteractiveTextNodeSpoiler], embeddedItems: [InteractiveTextNodeEmbeddedItem], attachments: [InteractiveTextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) { self.line = line + self.constrainedWidth = constrainedWidth self.frame = frame + self.intrinsicWidth = intrinsicWidth self.ascent = ascent self.descent = descent self.range = range + self.isTruncated = isTruncated self.isRTL = isRTL self.strikethroughs = strikethroughs self.spoilers = spoilers @@ -267,7 +282,7 @@ public final class InteractiveTextNodeLayoutArguments { public let textShadowBlur: CGFloat? public let textStroke: (UIColor, CGFloat)? public let displayContentsUnderSpoilers: Bool - public let customTruncationToken: NSAttributedString? + public let customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)? public let expandedBlocks: Set public init( @@ -287,7 +302,7 @@ public final class InteractiveTextNodeLayoutArguments { textShadowBlur: CGFloat? = nil, textStroke: (UIColor, CGFloat)? = nil, displayContentsUnderSpoilers: Bool = false, - customTruncationToken: NSAttributedString? = nil, + customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)? = nil, expandedBlocks: Set = Set() ) { self.attributedString = attributedString @@ -1073,6 +1088,28 @@ private func addAttachment(attachment: UIImage, line: InteractiveTextNodeLine, a } open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecognizerDelegate { + public final class ApplyArguments { + public let animation: ListViewItemUpdateAnimation + public let spoilerTextColor: UIColor + public let spoilerEffectColor: UIColor + public let areContentAnimationsEnabled: Bool + public let spoilerExpandRect: CGRect? + + public init( + animation: ListViewItemUpdateAnimation, + spoilerTextColor: UIColor, + spoilerEffectColor: UIColor, + areContentAnimationsEnabled: Bool, + spoilerExpandRect: CGRect? + ) { + self.animation = animation + self.spoilerTextColor = spoilerTextColor + self.spoilerEffectColor = spoilerEffectColor + self.areContentAnimationsEnabled = areContentAnimationsEnabled + self.spoilerExpandRect = spoilerExpandRect + } + } + public struct RenderContentTypes: OptionSet { public var rawValue: Int @@ -1106,7 +1143,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn public var canHandleTapAtPoint: ((CGPoint) -> Bool)? public var requestToggleBlockCollapsed: ((Int) -> Void)? - public var requestDisplayContentsUnderSpoilers: (() -> Void)? + public var requestDisplayContentsUnderSpoilers: ((CGPoint?) -> Void)? private var tapRecognizer: UITapGestureRecognizer? public var currentText: NSAttributedString? { @@ -1146,10 +1183,10 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn continue } - guard let item = contentItemLayer.item else { + guard let params = contentItemLayer.params else { continue } - guard let blockQuote = item.segment.blockQuote else { + guard let blockQuote = params.item.segment.blockQuote else { continue } if blockQuote.isCollapsed == nil { @@ -1232,7 +1269,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displayContentsUnderSpoilers: Bool, - customTruncationToken: NSAttributedString?, + customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)?, expandedBlocks: Set ) -> InteractiveTextNodeLayout { let blockQuoteLeftInset: CGFloat = 9.0 @@ -1324,7 +1361,8 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn } } - struct CalculatedSegment { + class CalculatedSegment { + let id: Int? var titleLine: InteractiveTextNodeLine? var lines: [InteractiveTextNodeLine] = [] var tintColor: UIColor? @@ -1332,16 +1370,31 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn var tertiaryTintColor: UIColor? var blockQuote: TextNodeBlockQuoteData? var additionalWidth: CGFloat = 0.0 + + init(id: Int?) { + self.id = id + } } var calculatedSegments: [CalculatedSegment] = [] + var remainingLines = maximumNumberOfLines <= 0 ? Int.max : maximumNumberOfLines + var nextBlockIndex = 0 for segment in stringSegments { - var calculatedSegment = CalculatedSegment() - calculatedSegment.blockQuote = segment.blockQuote - calculatedSegment.tintColor = segment.tintColor - calculatedSegment.secondaryTintColor = segment.secondaryTintColor - calculatedSegment.tertiaryTintColor = segment.tertiaryTintColor + if remainingLines <= 0 { + break + } + + var blockIndex: Int? + var isCollapsed = false + if let blockQuote = segment.blockQuote { + let blockIndexValue = nextBlockIndex + blockIndex = blockIndexValue + nextBlockIndex += 1 + if blockQuote.isCollapsible { + isCollapsed = !expandedBlocks.contains(blockIndexValue) + } + } let rawSubstring = segment.substring.string as NSString let substringLength = rawSubstring.length @@ -1352,6 +1405,12 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn var currentLineStartIndex = segment.firstCharacterOffset let segmentEndIndex = segment.firstCharacterOffset + substringLength + let calculatedSegment = CalculatedSegment(id: blockIndex) + calculatedSegment.blockQuote = segment.blockQuote + calculatedSegment.tintColor = segment.tintColor + calculatedSegment.secondaryTintColor = segment.secondaryTintColor + calculatedSegment.tertiaryTintColor = segment.tertiaryTintColor + var constrainedSegmentWidth = constrainedSize.width var additionalOffsetX: CGFloat = 0.0 if segment.blockQuote != nil { @@ -1374,16 +1433,20 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn if let title = segment.title { let rawTitleLine = CTLineCreateWithAttributedString(title) - if let titleLine = CTLineCreateTruncatedLine(rawTitleLine, constrainedSegmentWidth - additionalSegmentRightInset, .end, nil) { + let constrainedLineWidth = constrainedSegmentWidth - additionalSegmentRightInset + if let titleLine = CTLineCreateTruncatedLine(rawTitleLine, constrainedLineWidth, .end, nil) { var lineAscent: CGFloat = 0.0 var lineDescent: CGFloat = 0.0 let lineWidth = CTLineGetTypographicBounds(titleLine, &lineAscent, &lineDescent, nil) calculatedSegment.titleLine = InteractiveTextNodeLine( line: titleLine, + constrainedWidth: constrainedLineWidth, frame: CGRect(origin: CGPoint(x: additionalOffsetX, y: 0.0), size: CGSize(width: lineWidth + additionalSegmentRightInset, height: lineAscent + lineDescent)), + intrinsicWidth: lineWidth, ascent: lineAscent, descent: lineDescent, range: nil, + isTruncated: false, isRTL: false, strikethroughs: [], spoilers: [], @@ -1397,7 +1460,8 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn } while true { - let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, currentLineStartIndex, constrainedSegmentWidth - additionalSegmentRightInset) + let constrainedLineWidth = constrainedSegmentWidth - additionalSegmentRightInset + let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, currentLineStartIndex, constrainedLineWidth) if lineCharacterCount != 0 { let line = CTTypesetterCreateLine(typesetter, CFRange(location: currentLineStartIndex, length: lineCharacterCount)) @@ -1417,10 +1481,13 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn calculatedSegment.lines.append(InteractiveTextNodeLine( line: line, + constrainedWidth: constrainedLineWidth, frame: CGRect(origin: CGPoint(x: additionalOffsetX, y: 0.0), size: CGSize(width: lineWidth + additionalSegmentRightInset, height: lineAscent + lineDescent)), + intrinsicWidth: lineWidth, ascent: lineAscent, descent: lineDescent, range: NSRange(location: currentLineStartIndex, length: lineCharacterCount), + isTruncated: false, isRTL: isRTL && segment.blockQuote == nil, strikethroughs: [], spoilers: [], @@ -1429,6 +1496,11 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn attachments: [], additionalTrailingLine: nil )) + + remainingLines -= 1 + if remainingLines <= 0 { + break + } } additionalSegmentRightInset = 0.0 @@ -1438,11 +1510,108 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn if currentLineStartIndex >= segmentEndIndex { break } + if remainingLines <= 0 { + break + } + } + + if isCollapsed, calculatedSegment.lines.count > 3 { + let lastLine = calculatedSegment.lines[2] + if !lastLine.isTruncated, let lineRange = lastLine.range, let lineFont = attributedString.attribute(.font, at: lineRange.lowerBound, effectiveRange: nil) as? UIFont { + var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:] + truncationTokenAttributes[NSAttributedString.Key.font] = lineFont + truncationTokenAttributes[NSAttributedString.Key(rawValue: kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber + let tokenString = "\u{2026}" + + let truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes) + + let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString) + + var truncationTokenAscent: CGFloat = 0.0 + var truncationTokenDescent: CGFloat = 0.0 + let truncationTokenWidth = CTLineGetTypographicBounds(truncationToken, &truncationTokenAscent, &truncationTokenDescent, nil) + + if let updatedLine = CTLineCreateTruncatedLine(lastLine.line, max(0.0, lastLine.constrainedWidth - truncationTokenWidth), .end, nil) { + var lineAscent: CGFloat = 0.0 + var lineDescent: CGFloat = 0.0 + var lineWidth = CTLineGetTypographicBounds(updatedLine, &lineAscent, &lineDescent, nil) + lineWidth = min(lineWidth, lastLine.constrainedWidth) + + calculatedSegment.lines[2] = InteractiveTextNodeLine( + line: updatedLine, + constrainedWidth: lastLine.constrainedWidth, + frame: CGRect(origin: lastLine.frame.origin, size: CGSize(width: lineWidth, height: lineAscent + lineDescent)), + intrinsicWidth: lineWidth, + ascent: lineAscent, + descent: lineDescent, + range: lastLine.range, + isTruncated: true, + isRTL: lastLine.isRTL, + strikethroughs: [], + spoilers: [], + spoilerWords: [], + embeddedItems: [], + attachments: [], + additionalTrailingLine: (truncationToken, 0.0) + ) + } + } } calculatedSegments.append(calculatedSegment) } + if remainingLines <= 0, let lastSegment = calculatedSegments.last, let lastLine = lastSegment.lines.last, !lastLine.isTruncated, let lineRange = lastLine.range, let lineFont = attributedString.attribute(.font, at: lineRange.lowerBound, effectiveRange: nil) as? UIFont { + if let range = lastLine.range, range.upperBound != attributedString.length { + let truncatedTokenString: NSAttributedString + if let customTruncationTokenValue = customTruncationToken?(lineFont, lastSegment.blockQuote != nil) { + if lineRange.length == 0 && customTruncationTokenValue.string.hasPrefix("\u{2026} ") { + truncatedTokenString = customTruncationTokenValue.attributedSubstring(from: NSRange(location: 2, length: customTruncationTokenValue.length - 2)) + } else { + truncatedTokenString = customTruncationTokenValue + } + } else { + var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:] + truncationTokenAttributes[NSAttributedString.Key.font] = lineFont + truncationTokenAttributes[NSAttributedString.Key(rawValue: kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber + let tokenString = "\u{2026}" + + truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes) + } + + let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString) + + var truncationTokenAscent: CGFloat = 0.0 + var truncationTokenDescent: CGFloat = 0.0 + let truncationTokenWidth = CTLineGetTypographicBounds(truncationToken, &truncationTokenAscent, &truncationTokenDescent, nil) + + if let updatedLine = CTLineCreateTruncatedLine(lastLine.line, max(0.0, lastLine.constrainedWidth - truncationTokenWidth), .end, nil) { + var lineAscent: CGFloat = 0.0 + var lineDescent: CGFloat = 0.0 + var lineWidth = CTLineGetTypographicBounds(updatedLine, &lineAscent, &lineDescent, nil) + lineWidth = min(lineWidth, lastLine.constrainedWidth) + + lastSegment.lines[lastSegment.lines.count - 1] = InteractiveTextNodeLine( + line: updatedLine, + constrainedWidth: lastLine.constrainedWidth, + frame: CGRect(origin: lastLine.frame.origin, size: CGSize(width: lineWidth, height: lineAscent + lineDescent)), + intrinsicWidth: lineWidth, + ascent: lineAscent, + descent: lineDescent, + range: lastLine.range, + isTruncated: true, + isRTL: lastLine.isRTL, + strikethroughs: [], + spoilers: [], + spoilerWords: [], + embeddedItems: [], + attachments: [], + additionalTrailingLine: (truncationToken, 0.0) + ) + } + } + } + var size = CGSize() let isTruncated = false @@ -1451,7 +1620,11 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn size.width = max(size.width, titleLine.frame.origin.x + titleLine.frame.width + segment.additionalWidth) } for line in segment.lines { - size.width = max(size.width, line.frame.origin.x + line.frame.width + segment.additionalWidth) + var additionalTrailingWidth: CGFloat = 0.0 + if let additionalTrailingLine = line.additionalTrailingLine { + additionalTrailingWidth += CTLineGetTypographicBounds(additionalTrailingLine.0, nil, nil, nil) + } + size.width = max(size.width, line.frame.origin.x + line.frame.width + segment.additionalWidth + additionalTrailingWidth) } } @@ -1459,8 +1632,6 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn var firstLineOffset: CGFloat? - var nextBlockIndex = 0 - for i in 0 ..< calculatedSegments.count { var segmentLines: [InteractiveTextNodeLine] = [] @@ -1475,7 +1646,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn } } - let blockMinY = size.height - insets.bottom + let blockMinY = size.height var blockWidth: CGFloat = 0.0 if let titleLine = segment.titleLine { @@ -1487,14 +1658,11 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn segmentLines.append(titleLine) } - var blockIndex: Int? + let blockIndex = segment.id var isCollapsed = false - if let blockQuote = segment.blockQuote { - let blockIndexValue = nextBlockIndex - blockIndex = blockIndexValue - nextBlockIndex += 1 + if let blockIndex, let blockQuote = segment.blockQuote { if blockQuote.isCollapsible { - isCollapsed = !expandedBlocks.contains(blockIndexValue) + isCollapsed = !expandedBlocks.contains(blockIndex) } } @@ -1506,21 +1674,28 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn let line = segment.lines[i] lineCount += 1 + if i != 0 { + segmentHeight += line.frame.height * lineSpacingFactor + } + if isCollapsed && lineCount > 3 { + } else { + effectiveSegmentHeight += line.frame.height * lineSpacingFactor + } + line.frame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: size.height + segmentHeight), size: line.frame.size) line.frame.size.width += max(0.0, segment.additionalWidth) - var lineHeightIncrease = line.frame.height - if i != segment.lines.count - 1 { - lineHeightIncrease += line.frame.height * lineSpacingFactor - } - - segmentHeight += lineHeightIncrease + segmentHeight += line.frame.height if isCollapsed && lineCount > 3 { } else { - effectiveSegmentHeight += lineHeightIncrease + effectiveSegmentHeight += line.frame.height visibleLineCount = i + 1 } - blockWidth = max(blockWidth, line.frame.origin.x + line.frame.width) + var additionalTrailingWidth: CGFloat = 0.0 + if let additionalTrailingLine = line.additionalTrailingLine { + additionalTrailingWidth += CTLineGetTypographicBounds(additionalTrailingLine.0, nil, nil, nil) + } + blockWidth = max(blockWidth, line.frame.origin.x + line.frame.width + additionalTrailingWidth) if let range = line.range { attributedString.enumerateAttributes(in: range, options: []) { attributes, range, _ in @@ -1605,7 +1780,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn effectiveSegmentHeight = ceil(effectiveSegmentHeight) size.height += effectiveSegmentHeight - let blockMaxY = size.height - insets.bottom + let blockMaxY = size.height if i != calculatedSegments.count - 1 { if segment.blockQuote != nil { @@ -1618,8 +1793,20 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn } var segmentBlockQuote: InteractiveTextNodeBlockQuote? - if let blockQuote = segment.blockQuote, let tintColor = segment.tintColor, let blockIndex { - segmentBlockQuote = InteractiveTextNodeBlockQuote(id: blockIndex, frame: CGRect(origin: CGPoint(x: 0.0, y: blockMinY - 2.0), size: CGSize(width: blockWidth, height: blockMaxY - (blockMinY - 2.0) + 3.0)), data: blockQuote, tintColor: tintColor, secondaryTintColor: segment.secondaryTintColor, tertiaryTintColor: segment.tertiaryTintColor, backgroundColor: blockQuote.backgroundColor, isCollapsed: (blockQuote.isCollapsible && segmentLines.count > 3) ? isCollapsed : nil) + if let blockQuote = segment.blockQuote, let tintColor = segment.tintColor, let blockIndex, let firstLine = segment.lines.first, let lastLine = segment.lines.last { + segmentBlockQuote = InteractiveTextNodeBlockQuote( + id: blockIndex, + frame: CGRect( + origin: CGPoint(x: 0.0, y: blockMinY - floor(firstLine.frame.height * 0.2)), + size: CGSize(width: blockWidth, height: blockMaxY - blockMinY + floor(firstLine.frame.height * 0.2) + floor(lastLine.frame.height * 0.15)) + ), + data: blockQuote, + tintColor: tintColor, + secondaryTintColor: segment.secondaryTintColor, + tertiaryTintColor: segment.tertiaryTintColor, + backgroundColor: blockQuote.backgroundColor, + isCollapsed: (blockQuote.isCollapsible && segmentLines.count > 3) ? isCollapsed : nil + ) } segments.append(InteractiveTextNodeSegment( @@ -1668,7 +1855,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn ) } - static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displayContentsUnderSpoilers: Bool, customTruncationToken: NSAttributedString?, expandedBlocks: Set) -> InteractiveTextNodeLayout { + static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displayContentsUnderSpoilers: Bool, customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)?, expandedBlocks: Set) -> InteractiveTextNodeLayout { guard let attributedString else { return InteractiveTextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, segments: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, expandedBlocks: expandedBlocks) } @@ -1676,12 +1863,12 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn return calculateLayoutV2(attributedString: attributedString, minimumNumberOfLines: minimumNumberOfLines, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, verticalAlignment: verticalAlignment, lineSpacingFactor: lineSpacingFactor, cutout: cutout, insets: insets, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, customTruncationToken: customTruncationToken, expandedBlocks: expandedBlocks) } - private func updateContentItems(animation: ListViewItemUpdateAnimation) { + private func updateContentItems(arguments: ApplyArguments) { guard let cachedLayout = self.cachedLayout else { return } - let animateContents = self.isDisplayingContentsUnderSpoilers != nil && self.isDisplayingContentsUnderSpoilers != cachedLayout.displayContentsUnderSpoilers && animation.isAnimated + let animateContents = self.isDisplayingContentsUnderSpoilers != nil && self.isDisplayingContentsUnderSpoilers != cachedLayout.displayContentsUnderSpoilers && arguments.animation.isAnimated let synchronous = animateContents self.isDisplayingContentsUnderSpoilers = cachedLayout.displayContentsUnderSpoilers @@ -1698,6 +1885,11 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn var lineRect = line.frame lineRect.origin.y = topLeftOffset.y + line.frame.minY lineRect.origin.x = topLeftOffset.x + line.frame.minX + + if let additionalTrailingLine = line.additionalTrailingLine { + lineRect.size.width += CTLineGetTypographicBounds(additionalTrailingLine.0, nil, nil, nil) + } + if segmentRect.isEmpty { segmentRect = lineRect } else { @@ -1727,10 +1919,17 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn let contentItemFrame = CGRect(origin: CGPoint(x: segmentRect.minX, y: segmentRect.minY), size: CGSize(width: contentItem.size.width, height: contentItem.size.height)) - var contentItemAnimation = animation + var contentItemAnimation = arguments.animation let contentItemLayer: TextContentItemLayer + var itemSpoilerExpandRect: CGRect? + var itemAnimateContents = animateContents && contentItemAnimation.isAnimated if let current = self.contentItemLayers[itemId] { contentItemLayer = current + + if arguments.animation.isAnimated, let spoilerExpandRect = arguments.spoilerExpandRect { + itemSpoilerExpandRect = spoilerExpandRect.offsetBy(dx: -contentItemFrame.minX, dy: -contentItemFrame.minY) + itemAnimateContents = true + } } else { contentItemAnimation = .None contentItemLayer = TextContentItemLayer() @@ -1738,7 +1937,18 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn self.layer.addSublayer(contentItemLayer) } - contentItemLayer.update(item: contentItem, animation: contentItemAnimation, synchronously: synchronous, animateContents: animateContents && contentItemAnimation.isAnimated) + contentItemLayer.update( + params: TextContentItemLayer.Params( + item: contentItem, + spoilerTextColor: arguments.spoilerTextColor, + spoilerEffectColor: arguments.spoilerEffectColor, + areContentAnimationsEnabled: arguments.areContentAnimationsEnabled + ), + animation: contentItemAnimation, + synchronously: synchronous, + animateContents: itemAnimateContents, + spoilerExpandRect: itemSpoilerExpandRect + ) contentItemAnimation.animator.updateFrame(layer: contentItemLayer, frame: contentItemFrame, completion: nil) } @@ -1779,7 +1989,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn let point = recognizer.location(in: self.view) if let cachedLayout = self.cachedLayout, !cachedLayout.displayContentsUnderSpoilers, let (_, attributes) = self.attributesAtPoint(point) { if attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil { - self.requestDisplayContentsUnderSpoilers?() + self.requestDisplayContentsUnderSpoilers?(point) return } } @@ -1789,7 +1999,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn } } - public static func asyncLayout(_ maybeNode: InteractiveTextNode?) -> (InteractiveTextNodeLayoutArguments) -> (InteractiveTextNodeLayout, (ListViewItemUpdateAnimation) -> InteractiveTextNode) { + public static func asyncLayout(_ maybeNode: InteractiveTextNode?) -> (InteractiveTextNodeLayoutArguments) -> (InteractiveTextNodeLayout, (ApplyArguments) -> InteractiveTextNode) { let existingLayout: InteractiveTextNodeLayout? = maybeNode?.cachedLayout return { arguments in @@ -1831,10 +2041,10 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn let node = maybeNode ?? InteractiveTextNode() - return (layout, { animation in + return (layout, { arguments in if node.cachedLayout !== layout { node.cachedLayout = layout - node.updateContentItems(animation: animation) + node.updateContentItems(arguments: arguments) } return node @@ -1878,6 +2088,25 @@ final class TextContentItem { } final class TextContentItemLayer: SimpleLayer { + final class Params { + let item: TextContentItem + let spoilerTextColor: UIColor + let spoilerEffectColor: UIColor + let areContentAnimationsEnabled: Bool + + init( + item: TextContentItem, + spoilerTextColor: UIColor, + spoilerEffectColor: UIColor, + areContentAnimationsEnabled: Bool + ) { + self.item = item + self.spoilerTextColor = spoilerTextColor + self.spoilerEffectColor = spoilerEffectColor + self.areContentAnimationsEnabled = areContentAnimationsEnabled + } + } + final class RenderMask { let image: UIImage let isOpaque: Bool @@ -2090,27 +2319,8 @@ final class TextContentItemLayer: SimpleLayer { } } - if !line.strikethroughs.isEmpty { - for strikethrough in line.strikethroughs { - guard let lineRange = line.range else { - continue - } - var textColor: UIColor? - params.item.attributedString?.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in - if range == strikethrough.range, let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor { - textColor = color - } - } - if let textColor = textColor { - context.setFillColor(textColor.cgColor) - } - let frame = strikethrough.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY) - context.fill(CGRect(x: frame.minX, y: frame.minY - 5.0, width: frame.width, height: 1.0)) - } - } - if let (additionalTrailingLine, _) = line.additionalTrailingLine { - context.textPosition = CGPoint(x: lineFrame.maxX, y: lineFrame.minY) + context.textPosition = CGPoint(x: lineFrame.minX + line.intrinsicWidth, y: lineFrame.maxY - line.descent) let glyphRuns = CTLineGetGlyphRuns(additionalTrailingLine) as NSArray if glyphRuns.count != 0 { @@ -2172,10 +2382,15 @@ final class TextContentItemLayer: SimpleLayer { } } - private(set) var item: TextContentItem? + private(set) var params: Params? let renderNode: RenderNode private var contentMaskNode: ASImageNode? + + private var overlayContentLayer: SimpleLayer? + private var overlayContentMaskNode: ASImageNode? + private var spoilerEffectNode: InvisibleInkDustNode? + private var blockBackgroundView: MessageInlineBlockBackgroundView? private var quoteTypeIconNode: ASImageNode? private var blockExpandArrow: SimpleLayer? @@ -2202,14 +2417,20 @@ final class TextContentItemLayer: SimpleLayer { fatalError("init(coder:) has not been implemented") } - func update(item: TextContentItem, animation: ListViewItemUpdateAnimation, synchronously: Bool = false, animateContents: Bool = false) { - self.item = item + func update( + params: Params, + animation: ListViewItemUpdateAnimation, + synchronously: Bool, + animateContents: Bool, + spoilerExpandRect: CGRect? + ) { + self.params = params - let contentFrame = CGRect(origin: CGPoint(), size: item.size) + let contentFrame = CGRect(origin: CGPoint(), size: params.item.size) var effectiveContentFrame = contentFrame var contentMask: RenderMask? - if let blockQuote = item.segment.blockQuote { + if let blockQuote = params.item.segment.blockQuote { let blockBackgroundView: MessageInlineBlockBackgroundView if let current = self.blockBackgroundView { blockBackgroundView = current @@ -2230,19 +2451,28 @@ final class TextContentItemLayer: SimpleLayer { } blockExpandArrow.layerTintColor = blockQuote.tintColor.cgColor - let blockBackgroundFrame = blockQuote.frame.offsetBy(dx: item.contentOffset.x, dy: item.contentOffset.y + 4.0) + let blockBackgroundFrame = blockQuote.frame.offsetBy(dx: params.item.contentOffset.x, dy: params.item.contentOffset.y) if animation.isAnimated { - self.isAnimating = true - self.currentAnimationId += 1 - let animationId = self.currentAnimationId - animation.animator.updateFrame(layer: blockBackgroundView.layer, frame: blockBackgroundFrame, completion: { [weak self] completed in - guard completed, let self, self.currentAnimationId == animationId, let item = self.item else { - return - } - self.isAnimating = false - self.update(item: item, animation: .None, synchronously: true) - }) + if blockBackgroundFrame != blockBackgroundView.layer.frame { + self.isAnimating = true + self.currentAnimationId += 1 + let animationId = self.currentAnimationId + + animation.animator.updateFrame(layer: blockBackgroundView.layer, frame: blockBackgroundFrame, completion: { [weak self] completed in + guard completed, let self, self.currentAnimationId == animationId, let params = self.params else { + return + } + self.isAnimating = false + self.update( + params: params, + animation: .None, + synchronously: true, + animateContents: false, + spoilerExpandRect: nil + ) + }) + } } else { blockBackgroundView.layer.frame = blockBackgroundFrame } @@ -2293,7 +2523,7 @@ final class TextContentItemLayer: SimpleLayer { animation.animator.updateBounds(layer: blockExpandArrow, bounds: CGRect(origin: CGPoint(), size: expandArrowFrame.size), completion: nil) animation.animator.updateTransform(layer: blockExpandArrow, transform: CATransform3DMakeRotation(isCollapsed ? 0.0 : CGFloat.pi, 0.0, 0.0, 1.0), completion: nil) - let contentMaskFrame = CGRect(origin: CGPoint(x: 0.0, y: blockBackgroundFrame.minY - contentFrame.minY), size: CGSize(width: contentFrame.width, height: blockBackgroundFrame.height)) + let contentMaskFrame = CGRect(origin: CGPoint(x: 0.0, y: contentFrame.minY - blockBackgroundFrame.minY), size: CGSize(width: contentFrame.width, height: blockBackgroundFrame.height)) contentMask = RenderMask(image: expandableBlockMaskImage, isOpaque: !isCollapsed, frame: contentMaskFrame) effectiveContentFrame.size.height = ceil(contentMaskFrame.height - contentMaskFrame.minY) } else { @@ -2353,22 +2583,278 @@ final class TextContentItemLayer: SimpleLayer { } else { if let contentMaskNode = self.contentMaskNode { self.contentMaskNode = nil - self.renderNode.layer.mask = nil contentMaskNode.layer.removeFromSuperlayer() } + self.renderNode.layer.mask = nil + } + + if !params.item.segment.spoilers.isEmpty { + let spoilerEffectNode: InvisibleInkDustNode + if let current = self.spoilerEffectNode { + spoilerEffectNode = current + } else { + spoilerEffectNode = InvisibleInkDustNode(textNode: nil, enableAnimations: params.areContentAnimationsEnabled) + self.spoilerEffectNode = spoilerEffectNode + } + + spoilerEffectNode.frame = contentFrame + spoilerEffectNode.update( + size: contentFrame.size, + color: params.spoilerEffectColor, + textColor: params.spoilerTextColor, + rects: params.item.segment.spoilers.map { $0.1.offsetBy(dx: 0.0 + params.item.contentOffset.x, dy: params.item.contentOffset.y + 0.0).insetBy(dx: 1.0, dy: 1.0) }, + wordRects: params.item.segment.spoilerWords.map { $0.1.offsetBy(dx: params.item.contentOffset.x + 0.0, dy: params.item.contentOffset.y + 0.0).insetBy(dx: 1.0, dy: 1.0) } + ) + } else { + if let spoilerEffectNode = self.spoilerEffectNode { + self.spoilerEffectNode = nil + spoilerEffectNode.layer.removeFromSuperlayer() + } + } + + if self.spoilerEffectNode != nil { + let overlayContentLayer: SimpleLayer + if let current = self.overlayContentLayer { + overlayContentLayer = current + animation.animator.updateFrame(layer: overlayContentLayer, frame: effectiveContentFrame, completion: nil) + } else { + overlayContentLayer = SimpleLayer() + self.overlayContentLayer = overlayContentLayer + overlayContentLayer.masksToBounds = true + self.addSublayer(overlayContentLayer) + overlayContentLayer.frame = effectiveContentFrame + } + + if let contentMask { + var overlayContentMaskAnimation = animation + let overlayContentMaskNode: ASImageNode + if let current = self.overlayContentMaskNode { + overlayContentMaskNode = current + } else { + overlayContentMaskNode = ASImageNode() + overlayContentMaskNode.isLayerBacked = true + overlayContentMaskNode.backgroundColor = .clear + self.overlayContentMaskNode = overlayContentMaskNode + overlayContentLayer.mask = overlayContentMaskNode.layer + + if let currentContentMask = self.currentContentMask { + overlayContentMaskNode.frame = currentContentMask.frame + } else { + overlayContentMaskAnimation = .None + } + + overlayContentMaskNode.image = contentMask.image + } + + overlayContentMaskAnimation.animator.updateBackgroundColor(layer: overlayContentMaskNode.layer, color: contentMask.isOpaque ? UIColor.white : UIColor.clear, completion: nil) + overlayContentMaskAnimation.animator.updateFrame(layer: overlayContentMaskNode.layer, frame: contentMask.frame, completion: nil) + } else { + if let _ = self.overlayContentMaskNode { + self.overlayContentMaskNode = nil + overlayContentLayer.mask = nil + } + } + + if let spoilerEffectNode = self.spoilerEffectNode { + if spoilerEffectNode.layer.superlayer !== overlayContentLayer { + overlayContentLayer.addSublayer(spoilerEffectNode.layer) + } + } + } else { + if let overlayContentLayer = self.overlayContentLayer { + self.overlayContentLayer = nil + overlayContentLayer.removeFromSuperlayer() + } } self.currentContentMask = contentMask - self.renderNode.params = RenderParams(size: contentFrame.size, item: item, mask: staticContentMask) + self.renderNode.params = RenderParams(size: contentFrame.size, item: params.item, mask: staticContentMask) if synchronously { - let previousContents = self.renderNode.layer.contents - self.renderNode.displayImmediately() - if animateContents, let previousContents { - animation.transition.animateContents(layer: self.renderNode.layer, from: previousContents) + if let spoilerExpandRect, animation.isAnimated { + let localSpoilerExpandRect = spoilerExpandRect.offsetBy(dx: -self.renderNode.frame.minX, dy: -self.renderNode.frame.minY) + + let revealAnimationDuration: CGFloat = 0.55 + + let revealTransition: ContainedViewLayoutTransition = .animated(duration: revealAnimationDuration, curve: .easeInOut) + + let previousContents = self.renderNode.layer.contents + let copyContentsLayer = SimpleLayer() + copyContentsLayer.frame = self.renderNode.frame + copyContentsLayer.contents = previousContents + copyContentsLayer.masksToBounds = self.renderNode.layer.masksToBounds + copyContentsLayer.contentsGravity = self.renderNode.layer.contentsGravity + copyContentsLayer.contentsScale = self.renderNode.layer.contentsScale + for sublayer in self.renderNode.layer.sublayers ?? [] { + let copySublayer = SimpleLayer() + copySublayer.contentsScale = sublayer.contentsScale + copySublayer.position = sublayer.position + copySublayer.bounds = sublayer.bounds + copySublayer.transform = sublayer.transform + copySublayer.opacity = sublayer.opacity + copySublayer.isHidden = sublayer.isHidden + + if let sublayer = sublayer as? InlineStickerItemLayer { + sublayer.mirrorLayer = copySublayer + } else { + copySublayer.contents = sublayer.contents + } + + copyContentsLayer.addSublayer(copySublayer) + } + self.renderNode.layer.superlayer?.insertSublayer(copyContentsLayer, below: self.renderNode.layer) + + self.renderNode.displayImmediately() + + let rectangularExpandedSide = max(localSpoilerExpandRect.width, localSpoilerExpandRect.height) + // The gradient starts at 0.7 + let adjustedExpandedSide = ceil(rectangularExpandedSide * 1.3) + + let rectangularExpandedRect = CGSize(width: adjustedExpandedSide, height: adjustedExpandedSide).centered(around: spoilerExpandRect.center) + + let maskFrame = self.renderNode.bounds + + let maskLayer = SimpleLayer() + maskLayer.masksToBounds = true + self.renderNode.layer.mask = maskLayer + maskLayer.frame = maskFrame + + animateRadialExpansionMask(maskLayer: maskLayer, expandedRect: rectangularExpandedRect, transition: revealTransition, inverse: false, completion: { [weak self] in + guard let self, let params = self.params else { + return + } + self.renderNode.layer.mask = nil + self.update( + params: params, + animation: .None, + synchronously: true, + animateContents: false, + spoilerExpandRect: nil + ) + }) + + let copyMaskLayer = SimpleLayer() + copyMaskLayer.masksToBounds = true + copyContentsLayer.mask = copyMaskLayer + copyMaskLayer.frame = maskFrame + + animateRadialExpansionMask(maskLayer: copyMaskLayer, expandedRect: rectangularExpandedRect, transition: revealTransition, inverse: true, completion: { [weak copyContentsLayer] in + copyContentsLayer?.removeFromSuperlayer() + }) + + if let spoilerEffectNode = self.spoilerEffectNode { + let spoilerMaskLayer = SimpleLayer() + spoilerMaskLayer.masksToBounds = true + spoilerEffectNode.layer.mask = spoilerMaskLayer + spoilerMaskLayer.frame = maskFrame + + let spoilerLocalPosition = self.convert(rectangularExpandedRect.center, to: spoilerEffectNode.layer) + spoilerEffectNode.revealWithoutMaskAtLocation(spoilerLocalPosition) + + animateRadialExpansionMask(maskLayer: spoilerMaskLayer, expandedRect: rectangularExpandedRect, transition: revealTransition, inverse: true, completion: { [weak self] in + guard let self, let spoilerEffectNode = self.spoilerEffectNode else { + return + } + spoilerEffectNode.layer.mask = nil + spoilerEffectNode.layer.opacity = 0.0 + }) + } + } else { + let previousContents = self.renderNode.layer.contents + self.renderNode.displayImmediately() + if animateContents, let previousContents { + animation.transition.animateContents(layer: self.renderNode.layer, from: previousContents) + } + + if let spoilerEffectNode = self.spoilerEffectNode { + animation.transition.updateAlpha(layer: spoilerEffectNode.layer, alpha: params.item.displayContentsUnderSpoilers ? 0.0 : 1.0) + } } } else { self.renderNode.setNeedsDisplay() + + if let spoilerEffectNode = self.spoilerEffectNode { + animation.transition.updateAlpha(layer: spoilerEffectNode.layer, alpha: params.item.displayContentsUnderSpoilers ? 0.0 : 1.0) + spoilerEffectNode.update(revealed: params.item.displayContentsUnderSpoilers, animated: animation.isAnimated) + } } } } + +private func animateRadialExpansionMask(maskLayer: CALayer, expandedRect: CGRect, transition: ContainedViewLayoutTransition, inverse: Bool, completion: @escaping () -> Void) { + let maskGradientLayer = SimpleGradientLayer() + maskLayer.addSublayer(maskGradientLayer) + maskGradientLayer.frame = expandedRect + + setupSpoilerExpansionMaskGradient( + gradientLayer: maskGradientLayer, + centerLocation: CGPoint( + x: 0.5, + y: 0.5 + ), + radius: CGSize( + width: 0.5, + height: 0.5 + ), + inverse: inverse + ) + + let minGradientFrame = CGSize(width: 1.0, height: 1.0).centered(around: expandedRect.center) + + transition.animateFrame(layer: maskGradientLayer, from: minGradientFrame, delay: 0.1, completion: { _ in + completion() + }) + + if inverse { + let outerBoundsSourceRect = minGradientFrame.insetBy(dx: 0.5, dy: 0.5) + let outerBoundsDestinationRect = expandedRect.insetBy(dx: 0.5, dy: 0.5) + + for sideIndex in 0 ..< 4 { + let copyMaskOuterBoundsTopLayer = SimpleLayer() + copyMaskOuterBoundsTopLayer.backgroundColor = UIColor.white.cgColor + maskLayer.addSublayer(copyMaskOuterBoundsTopLayer) + + let sourceFrame: CGRect + let destinationFrame: CGRect + + // Top, left, bottom, right + if sideIndex == 0 { + sourceFrame = CGRect(origin: CGPoint(x: 0.0, y: outerBoundsSourceRect.minY - expandedRect.height), size: expandedRect.size) + destinationFrame = CGRect(origin: CGPoint(x: 0.0, y: outerBoundsDestinationRect.minY - expandedRect.height), size: expandedRect.size) + } else if sideIndex == 1 { + sourceFrame = CGRect(origin: CGPoint(x: outerBoundsSourceRect.minX - expandedRect.width, y: 0.0), size: expandedRect.size) + destinationFrame = CGRect(origin: CGPoint(x: outerBoundsDestinationRect.minX - expandedRect.width, y: 0.0), size: expandedRect.size) + } else if sideIndex == 2 { + sourceFrame = CGRect(origin: CGPoint(x: 0.0, y: outerBoundsSourceRect.maxY), size: expandedRect.size) + destinationFrame = CGRect(origin: CGPoint(x: 0.0, y: outerBoundsDestinationRect.maxY), size: expandedRect.size) + } else { + sourceFrame = CGRect(origin: CGPoint(x: outerBoundsSourceRect.maxX, y: 0.0), size: expandedRect.size) + destinationFrame = CGRect(origin: CGPoint(x: outerBoundsDestinationRect.maxX, y: 0.0), size: expandedRect.size) + } + + copyMaskOuterBoundsTopLayer.frame = destinationFrame + transition.animateFrame(layer: copyMaskOuterBoundsTopLayer, from: sourceFrame, delay: 0.1) + } + } +} + +private func setupSpoilerExpansionMaskGradient(gradientLayer: SimpleGradientLayer, centerLocation: CGPoint, radius: CGSize, inverse: Bool) { + let startAlpha: CGFloat = inverse ? 0.0 : 1.0 + let endAlpha: CGFloat = inverse ? 1.0 : 0.0 + + let locations: [CGFloat] = [0.0, 0.7, 0.95, 1.0] + let colors: [CGColor] = [ + UIColor(rgb: 0xff0000, alpha: startAlpha).cgColor, + UIColor(rgb: 0xff0000, alpha: startAlpha).cgColor, + UIColor(rgb: 0xff0000, alpha: endAlpha).cgColor, + UIColor(rgb: 0xff0000, alpha: endAlpha).cgColor + ] + + gradientLayer.type = .radial + gradientLayer.colors = colors + gradientLayer.locations = locations.map { $0 as NSNumber } + gradientLayer.startPoint = centerLocation + + let endEndPoint = CGPoint(x: (gradientLayer.startPoint.x + radius.width) * 1.0, y: (gradientLayer.startPoint.y + radius.height) * 1.0) + gradientLayer.endPoint = endEndPoint +} diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextNodeWithEntities.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextNodeWithEntities.swift index 77fd404cb17..cb92c3f9cba 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextNodeWithEntities.swift @@ -11,7 +11,6 @@ import AnimationCache import MultiAnimationRenderer import TelegramCore import EmojiTextAttachmentView -import InvisibleInkDustNode private final class InlineStickerItem: Hashable { let emoji: ChatTextInputTextCustomEmojiAttribute @@ -64,7 +63,7 @@ public final class InteractiveTextNodeWithEntities { public let attemptSynchronous: Bool public let textColor: UIColor public let spoilerEffectColor: UIColor - public let animation: ListViewItemUpdateAnimation + public let applyArguments: InteractiveTextNode.ApplyArguments public init( context: AccountContext, @@ -74,7 +73,7 @@ public final class InteractiveTextNodeWithEntities { attemptSynchronous: Bool, textColor: UIColor, spoilerEffectColor: UIColor, - animation: ListViewItemUpdateAnimation + applyArguments: InteractiveTextNode.ApplyArguments ) { self.context = context self.cache = cache @@ -83,7 +82,7 @@ public final class InteractiveTextNodeWithEntities { self.attemptSynchronous = attemptSynchronous self.textColor = textColor self.spoilerEffectColor = spoilerEffectColor - self.animation = animation + self.applyArguments = applyArguments } public func withUpdatedPlaceholderColor(_ color: UIColor) -> Arguments { @@ -95,7 +94,7 @@ public final class InteractiveTextNodeWithEntities { attemptSynchronous: self.attemptSynchronous, textColor: self.textColor, spoilerEffectColor: self.spoilerEffectColor, - animation: self.animation + applyArguments: self.applyArguments ) } } @@ -112,7 +111,7 @@ public final class InteractiveTextNodeWithEntities { public let textNode: InteractiveTextNode private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayerData] = [:] - private var dustEffectNodes: [Int: InvisibleInkDustNode] = [:] + private var displayContentsUnderSpoilers: Bool? private var enableLooping: Bool = true @@ -144,7 +143,7 @@ public final class InteractiveTextNodeWithEntities { self.textNode = textNode } - public static func asyncLayout(_ maybeNode: InteractiveTextNodeWithEntities?) -> (InteractiveTextNodeLayoutArguments) -> (InteractiveTextNodeLayout, (InteractiveTextNodeWithEntities.Arguments?) -> InteractiveTextNodeWithEntities) { + public static func asyncLayout(_ maybeNode: InteractiveTextNodeWithEntities?) -> (InteractiveTextNodeLayoutArguments) -> (InteractiveTextNodeLayout, (InteractiveTextNodeWithEntities.Arguments) -> InteractiveTextNodeWithEntities) { let makeLayout = InteractiveTextNode.asyncLayout(maybeNode?.textNode) return { [weak maybeNode] arguments in var updatedString: NSAttributedString? @@ -213,22 +212,40 @@ public final class InteractiveTextNodeWithEntities { let (layout, apply) = makeLayout(arguments.withAttributedString(updatedString)) return (layout, { applyArguments in - let animation: ListViewItemUpdateAnimation = applyArguments?.animation ?? .None + let animation: ListViewItemUpdateAnimation = applyArguments.applyArguments.animation - let result = apply(animation) + let result = apply(applyArguments.applyArguments) if let maybeNode = maybeNode { - if let applyArguments = applyArguments { - maybeNode.updateInteractiveContents(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false, textColor: applyArguments.textColor, spoilerEffectColor: applyArguments.spoilerEffectColor, animation: animation) - } + maybeNode.updateInteractiveContents( + context: applyArguments.context, + cache: applyArguments.cache, + renderer: applyArguments.renderer, + textLayout: layout, + placeholderColor: applyArguments.placeholderColor, + attemptSynchronousLoad: false, + textColor: applyArguments.textColor, + spoilerEffectColor: applyArguments.spoilerEffectColor, + animation: animation, + applyArguments: applyArguments.applyArguments + ) return maybeNode } else { let resultNode = InteractiveTextNodeWithEntities(textNode: result) - if let applyArguments = applyArguments { - resultNode.updateInteractiveContents(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false, textColor: applyArguments.textColor, spoilerEffectColor: applyArguments.spoilerEffectColor, animation: .None) - } + resultNode.updateInteractiveContents( + context: applyArguments.context, + cache: applyArguments.cache, + renderer: applyArguments.renderer, + textLayout: layout, + placeholderColor: applyArguments.placeholderColor, + attemptSynchronousLoad: false, + textColor: applyArguments.textColor, + spoilerEffectColor: applyArguments.spoilerEffectColor, + animation: .None, + applyArguments: applyArguments.applyArguments + ) return resultNode } @@ -253,7 +270,8 @@ public final class InteractiveTextNodeWithEntities { attemptSynchronousLoad: Bool, textColor: UIColor, spoilerEffectColor: UIColor, - animation: ListViewItemUpdateAnimation + animation: ListViewItemUpdateAnimation, + applyArguments: InteractiveTextNode.ApplyArguments ) { self.enableLooping = context.sharedContext.energyUsageSettings.loopEmoji @@ -262,15 +280,15 @@ public final class InteractiveTextNodeWithEntities { displayContentsUnderSpoilers = textLayout.displayContentsUnderSpoilers } + self.displayContentsUnderSpoilers = displayContentsUnderSpoilers + var nextIndexById: [Int64: Int] = [:] var validIds: [InlineStickerItemLayer.Key] = [] - var validDustEffectIds: [Int] = [] - if let textLayout { for i in 0 ..< textLayout.segments.count { let segment = textLayout.segments[i] - guard let segmentLayer = self.textNode.segmentLayer(index: i), let segmentItem = segmentLayer.item else { + guard let segmentLayer = self.textNode.segmentLayer(index: i), let segmentParams = segmentLayer.params else { continue } @@ -292,8 +310,8 @@ public final class InteractiveTextNodeWithEntities { itemFrame.origin.x = floorToScreenPixels(itemFrame.origin.x) itemFrame.origin.y = floorToScreenPixels(itemFrame.origin.y) - itemFrame.origin.x += segmentItem.contentOffset.x - itemFrame.origin.y += segmentItem.contentOffset.y + itemFrame.origin.x += segmentParams.item.contentOffset.x + itemFrame.origin.y += segmentParams.item.contentOffset.y let itemLayerData: InlineStickerItemLayerData var itemLayerTransition = animation.transition @@ -311,41 +329,14 @@ public final class InteractiveTextNodeWithEntities { self.inlineStickerItemLayers[id] = itemLayerData segmentLayer.renderNode.layer.addSublayer(itemLayerData.itemLayer) - itemLayerData.itemLayer.isVisibleForAnimations = self.enableLooping && self.isItemVisible(itemRect: itemFrame.offsetBy(dx: -segmentItem.contentOffset.x, dy: -segmentItem.contentOffset.x)) + itemLayerData.itemLayer.isVisibleForAnimations = self.enableLooping && self.isItemVisible(itemRect: itemFrame.offsetBy(dx: -segmentParams.item.contentOffset.x, dy: -segmentParams.item.contentOffset.x)) } itemLayerTransition.updateAlpha(layer: itemLayerData.itemLayer, alpha: item.isHiddenBySpoiler ? 0.0 : 1.0) itemLayerData.itemLayer.frame = itemFrame - itemLayerData.rect = itemFrame.offsetBy(dx: -segmentItem.contentOffset.x, dy: -segmentItem.contentOffset.y) - } - } - - if !segment.spoilers.isEmpty { - validDustEffectIds.append(i) - - let dustEffectNode: InvisibleInkDustNode - if let current = self.dustEffectNodes[i] { - dustEffectNode = current - if dustEffectNode.layer.superlayer !== segmentLayer.renderNode.layer { - segmentLayer.renderNode.layer.addSublayer(dustEffectNode.layer) - } - } else { - dustEffectNode = InvisibleInkDustNode(textNode: nil, enableAnimations: context.sharedContext.energyUsageSettings.fullTranslucency) - self.dustEffectNodes[i] = dustEffectNode - segmentLayer.renderNode.layer.addSublayer(dustEffectNode.layer) + itemLayerData.rect = itemFrame.offsetBy(dx: -segmentParams.item.contentOffset.x, dy: -segmentParams.item.contentOffset.y) } - let dustNodeFrame = CGRect(origin: CGPoint(), size: segmentItem.size).insetBy(dx: -3.0, dy: -3.0) - dustEffectNode.frame = dustNodeFrame - dustEffectNode.update( - size: dustNodeFrame.size, - color: spoilerEffectColor, - textColor: textColor, - rects: segment.spoilers.map { $0.1.offsetBy(dx: 3.0 + segmentItem.contentOffset.x, dy: segmentItem.contentOffset.y + 3.0).insetBy(dx: 1.0, dy: 1.0) }, - wordRects: segment.spoilerWords.map { $0.1.offsetBy(dx: segmentItem.contentOffset.x + 3.0, dy: segmentItem.contentOffset.y + 3.0).insetBy(dx: 1.0, dy: 1.0) } - ) - - animation.transition.updateAlpha(node: dustEffectNode, alpha: displayContentsUnderSpoilers ? 0.0 : 1.0) } } } @@ -360,16 +351,5 @@ public final class InteractiveTextNodeWithEntities { for key in removeKeys { self.inlineStickerItemLayers.removeValue(forKey: key) } - - var removeDustEffectIds: [Int] = [] - for (id, dustEffectNode) in self.dustEffectNodes { - if !validDustEffectIds.contains(id) { - removeDustEffectIds.append(id) - dustEffectNode.removeFromSupernode() - } - } - for id in removeDustEffectIds { - self.dustEffectNodes.removeValue(forKey: id) - } } } diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index a3d855082c4..70f131dbfb8 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -95,7 +95,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { } public func animate(_ view: UIView, frame: CGRect) { - let transition = Transition.spring(duration: 0.4) + let transition = ComponentTransition.spring(duration: 0.4) transition.setFrame(view: view, frame: frame) } @@ -198,7 +198,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { self.inputPanel.parentState = self.state let inputPanelSize = self.inputPanel.update( - transition: Transition(transition), + transition: ComponentTransition(transition), component: AnyComponent( MessageInputPanelComponent( externalState: self.inputPanelExternalState, diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift index 12ca89591c7..c149d857f30 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -213,12 +213,12 @@ public final class ListActionItemComponent: Component { self.layer.removeAnimation(forKey: "transform.scale") if animateScale { - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) transition.setScale(layer: self.layer, scale: topScale) } } else { if animateScale { - let transition = Transition(animation: .none) + let transition = ComponentTransition(animation: .none) transition.setScale(layer: self.layer, scale: 1.0) self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in @@ -242,7 +242,7 @@ public final class ListActionItemComponent: Component { self.action?() } - func update(size: CGSize, theme: PresentationTheme, isSelected: Bool, transition: Transition) { + func update(size: CGSize, theme: PresentationTheme, isSelected: Bool, transition: ComponentTransition) { let checkLayer: CheckLayer if let current = self.checkLayer { checkLayer = current @@ -328,14 +328,14 @@ public final class ListActionItemComponent: Component { return result } - func update(component: ListActionItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ListActionItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component let themeUpdated = component.theme !== previousComponent?.theme var customAccessorySize: CGSize? - var customAccessoryTransition: Transition = transition + var customAccessoryTransition: ComponentTransition = transition var contentLeftInset: CGFloat = 16.0 let contentRightInset: CGFloat @@ -808,7 +808,7 @@ public final class ListActionItemComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ListItemComponentAdaptor/Sources/ListItemComponentAdaptor.swift b/submodules/TelegramUI/Components/ListItemComponentAdaptor/Sources/ListItemComponentAdaptor.swift index ba4e12d70e7..0b8b5a87ead 100644 --- a/submodules/TelegramUI/Components/ListItemComponentAdaptor/Sources/ListItemComponentAdaptor.swift +++ b/submodules/TelegramUI/Components/ListItemComponentAdaptor/Sources/ListItemComponentAdaptor.swift @@ -46,9 +46,9 @@ public final class ListItemComponentAdaptor: Component { } public final class View: UIView { - private var itemNode: ListViewItemNode? + public var itemNode: ListViewItemNode? - func update(component: ListItemComponentAdaptor, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ListItemComponentAdaptor, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let item = component.itemImpl() if let itemNode = self.itemNode { @@ -125,7 +125,7 @@ public final class ListItemComponentAdaptor: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift b/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift index 0d7e1030070..c92687fbe52 100644 --- a/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift +++ b/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift @@ -70,7 +70,7 @@ public final class ListItemSliderSelectorComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ListItemSliderSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ListItemSliderSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -205,7 +205,7 @@ public final class ListItemSliderSelectorComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ListItemSwipeOptionContainer/Sources/ListItemSwipeOptionContainer.swift b/submodules/TelegramUI/Components/ListItemSwipeOptionContainer/Sources/ListItemSwipeOptionContainer.swift index eeec875a32a..a32dfa93de1 100644 --- a/submodules/TelegramUI/Components/ListItemSwipeOptionContainer/Sources/ListItemSwipeOptionContainer.swift +++ b/submodules/TelegramUI/Components/ListItemSwipeOptionContainer/Sources/ListItemSwipeOptionContainer.swift @@ -177,7 +177,7 @@ open class ListItemSwipeOptionContainer: UIView, UIGestureRecognizerDelegate { isExpanded: Bool, extendedWidth: CGFloat, sideInset: CGFloat, - transition: Transition, + transition: ComponentTransition, additive: Bool, revealFactor: CGFloat, animateIconMovement: Bool @@ -342,13 +342,13 @@ open class ListItemSwipeOptionContainer: UIView, UIGestureRecognizerDelegate { return CGSize(width: maxWidth * CGFloat(self.optionViews.count), height: constrainedSize.height) } - public func updateRevealOffset(offset: CGFloat, sideInset: CGFloat, transition: Transition) { + public func updateRevealOffset(offset: CGFloat, sideInset: CGFloat, transition: ComponentTransition) { self.revealOffset = offset self.sideInset = sideInset self.updateNodesLayout(transition: transition) } - private func updateNodesLayout(transition: Transition) { + private func updateNodesLayout(transition: ComponentTransition) { let size = self.bounds.size if size.width.isLessThanOrEqualTo(0.0) || self.optionViews.isEmpty { return @@ -480,7 +480,7 @@ open class ListItemSwipeOptionContainer: UIView, UIGestureRecognizerDelegate { private var allowAnyDirection: Bool = false - public var updateRevealOffset: ((CGFloat, Transition) -> Void)? + public var updateRevealOffset: ((CGFloat, ComponentTransition) -> Void)? public var revealOptionsInteractivelyOpened: (() -> Void)? public var revealOptionsInteractivelyClosed: (() -> Void)? public var revealOptionSelected: ((Option, Bool) -> Void)? @@ -779,7 +779,7 @@ open class ListItemSwipeOptionContainer: UIView, UIGestureRecognizerDelegate { } } - open func updateRevealOffsetInternal(offset: CGFloat, transition: Transition, completion: (() -> Void)? = nil) { + open func updateRevealOffsetInternal(offset: CGFloat, transition: ComponentTransition, completion: (() -> Void)? = nil) { self.revealOffset = offset guard let (size, leftInset, rightInset) = self.validLayout else { return @@ -856,7 +856,7 @@ open class ListItemSwipeOptionContainer: UIView, UIGestureRecognizerDelegate { if !self.revealOffset.isZero { self.recognizer?.becomeCancelled() } - let transition: Transition + let transition: ComponentTransition if animated { transition = .spring(duration: 0.3) } else { diff --git a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift index 1a9ccf535fa..0cfdd563cf9 100644 --- a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift +++ b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift @@ -52,7 +52,7 @@ public final class ListMultilineTextFieldItemComponent: Component { public let updated: ((String) -> Void)? public let returnKeyAction: (() -> Void)? public let backspaceKeyAction: (() -> Void)? - public let textUpdateTransition: Transition + public let textUpdateTransition: ComponentTransition public let tag: AnyObject? public init( @@ -72,7 +72,7 @@ public final class ListMultilineTextFieldItemComponent: Component { updated: ((String) -> Void)? = nil, returnKeyAction: (() -> Void)? = nil, backspaceKeyAction: (() -> Void)? = nil, - textUpdateTransition: Transition = .immediate, + textUpdateTransition: ComponentTransition = .immediate, tag: AnyObject? = nil ) { self.externalState = externalState @@ -203,7 +203,7 @@ public final class ListMultilineTextFieldItemComponent: Component { } } - func update(component: ListMultilineTextFieldItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ListMultilineTextFieldItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -377,7 +377,7 @@ public final class ListMultilineTextFieldItemComponent: Component { return size } - public func updateCustomPlaceholder(value: String, size: CGSize, transition: Transition) { + public func updateCustomPlaceholder(value: String, size: CGSize, transition: ComponentTransition) { guard let component = self.component else { return } @@ -427,7 +427,7 @@ public final class ListMultilineTextFieldItemComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift index e2727290a06..b8092ada4fb 100644 --- a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift +++ b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift @@ -29,9 +29,9 @@ public final class ListSectionContentView: UIView { public let id: AnyHashable public let itemView: ItemView public let size: CGSize - public let transition: Transition + public let transition: ComponentTransition - public init(id: AnyHashable, itemView: ItemView, size: CGSize, transition: Transition) { + public init(id: AnyHashable, itemView: ItemView, size: CGSize, transition: ComponentTransition) { self.id = id self.itemView = itemView self.size = size @@ -109,7 +109,7 @@ public final class ListSectionContentView: UIView { self.highlightedItemId = itemId if configuration.extendsItemHighlightToSection { - let transition: Transition + let transition: ComponentTransition let backgroundColor: UIColor if itemId != nil { transition = .immediate @@ -122,15 +122,15 @@ public final class ListSectionContentView: UIView { self.externalContentBackgroundView.updateColor(color: backgroundColor, transition: transition) } else { if let previousHighlightedItemId, let previousItemView = self.itemViews[previousHighlightedItemId] { - Transition.easeInOut(duration: 0.2).setBackgroundColor(layer: previousItemView.highlightLayer, color: .clear) + ComponentTransition.easeInOut(duration: 0.2).setBackgroundColor(layer: previousItemView.highlightLayer, color: .clear) } if let itemId, let itemView = self.itemViews[itemId] { - Transition.immediate.setBackgroundColor(layer: itemView.highlightLayer, color: configuration.theme.list.itemHighlightedBackgroundColor) + ComponentTransition.immediate.setBackgroundColor(layer: itemView.highlightLayer, color: configuration.theme.list.itemHighlightedBackgroundColor) } } } - public func update(configuration: Configuration, width: CGFloat, leftInset: CGFloat, readyItems: [ReadyItem], transition: Transition) -> UpdateResult { + public func update(configuration: Configuration, width: CGFloat, leftInset: CGFloat, readyItems: [ReadyItem], transition: ComponentTransition) -> UpdateResult { self.configuration = configuration switch configuration.background { @@ -376,7 +376,7 @@ public final class ListSectionComponent: Component { return self.contentView.itemViews[id]?.contents.view } - func update(component: ListSectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ListSectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let headerSideInset: CGFloat = 16.0 @@ -511,7 +511,7 @@ public final class ListSectionComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -576,7 +576,7 @@ public final class ListSubSectionComponent: Component { return self.contentView.itemViews[id]?.contents.view } - func update(component: ListSubSectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ListSubSectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component var contentHeight: CGFloat = 0.0 @@ -642,7 +642,7 @@ public final class ListSubSectionComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift index 3f6e7c42655..9a67b390160 100644 --- a/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift +++ b/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift @@ -144,7 +144,7 @@ public final class ListTextFieldItemComponent: Component { return false } - func update(component: ListTextFieldItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ListTextFieldItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -248,7 +248,7 @@ public final class ListTextFieldItemComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift index 28b991d616c..5fdf32e4587 100644 --- a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift +++ b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift @@ -408,7 +408,7 @@ public final class LottieComponent: Component { } } - func update(component: LottieComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: LottieComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component @@ -470,7 +470,7 @@ public final class LottieComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/LottieCpp/BUILD b/submodules/TelegramUI/Components/LottieCpp/BUILD deleted file mode 100644 index 9743934e12a..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/BUILD +++ /dev/null @@ -1,48 +0,0 @@ -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -objc_library( - name = "LottieCpp", - enable_modules = True, - module_name = "LottieCpp", - srcs = glob([ - "Sources/**/*.m", - "Sources/**/*.mm", - "Sources/**/*.h", - "Sources/**/*.c", - "Sources/**/*.cpp", - "Sources/**/*.hpp", - ]), - copts = [ - "-Werror", - "-I{}/Sources".format(package_name()), - ], - hdrs = glob([ - "PublicHeaders/**/*.h", - ]), - includes = [ - "PublicHeaders", - ], - deps = [ - ], - sdk_frameworks = [ - "Foundation", - ], - visibility = [ - "//visibility:public", - ], -) - -cc_library( - name = "LottieCppBinding", - srcs = [], - hdrs = glob([ - "PublicHeaders/**/*.h", - ]), - includes = [ - "PublicHeaders", - ], - copts = [], - visibility = ["//visibility:public"], - linkstatic = 1, - tags = ["swift_module=LottieCppBinding"], -) diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/BezierPath.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/BezierPath.h deleted file mode 100644 index b17f942761f..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/BezierPath.h +++ /dev/null @@ -1,155 +0,0 @@ -#ifndef BezierPath_h -#define BezierPath_h - -#ifdef __cplusplus - -#include -#include -#include -#include - -#include - -namespace lottie { - -struct BezierTrimPathPosition { - float start; - float end; - - explicit BezierTrimPathPosition(float start_, float end_); -}; - -class BezierPathContents: public std::enable_shared_from_this { -public: - explicit BezierPathContents(CurveVertex const &startPoint); - - BezierPathContents(); - - explicit BezierPathContents(lottiejson11::Json const &jsonAny) noexcept(false); - - BezierPathContents(const BezierPathContents&) = delete; - BezierPathContents& operator=(BezierPathContents&) = delete; - - lottiejson11::Json toJson() const; - - std::shared_ptr cgPath() const; - -public: - std::vector elements; - std::optional closed; - - float length(); - -private: - std::optional _length; - -public: - void moveToStartPoint(CurveVertex const &vertex); - void addVertex(CurveVertex const &vertex); - - void reserveCapacity(size_t capacity); - void setElementCount(size_t count); - void invalidateLength(); - - void addCurve(Vector2D const &toPoint, Vector2D const &outTangent, Vector2D const &inTangent); - void addLine(Vector2D const &toPoint); - void close(); - void addElement(PathElement const &pathElement); - void updateVertex(CurveVertex const &vertex, int atIndex, bool remeasure); - - /// Trims a path fromLength toLength with an offset. - /// - /// Length and offset are defined in the length coordinate space. - /// If any argument is outside the range of this path, then it will be looped over the path from finish to start. - /// - /// Cutting the curve when fromLength is less than toLength - /// x x x x - /// ~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooooooooooooooooooooooo------------------- - /// |Offset |fromLength toLength| | - /// - /// Cutting the curve when from Length is greater than toLength - /// x x x x x - /// oooooooooooooooooo--------------------~~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooo - /// | toLength| |Offset |fromLength | - /// - std::vector> trim(float fromLength, float toLength, float offsetLength); - - // MARK: Private - - std::vector> trimPathAtLengths(std::vector const &positions); -}; - -class BezierPath { -public: - explicit BezierPath(CurveVertex const &startPoint); - BezierPath(); - explicit BezierPath(lottiejson11::Json const &jsonAny) noexcept(false); - - lottiejson11::Json toJson() const; - - float length(); - - void moveToStartPoint(CurveVertex const &vertex); - void addVertex(CurveVertex const &vertex); - void reserveCapacity(size_t capacity); - void setElementCount(size_t count); - void invalidateLength(); - void addCurve(Vector2D const &toPoint, Vector2D const &outTangent, Vector2D const &inTangent); - void addLine(Vector2D const &toPoint); - void close(); - void addElement(PathElement const &pathElement); - void updateVertex(CurveVertex const &vertex, int atIndex, bool remeasure); - - /// Trims a path fromLength toLength with an offset. - /// - /// Length and offset are defined in the length coordinate space. - /// If any argument is outside the range of this path, then it will be looped over the path from finish to start. - /// - /// Cutting the curve when fromLength is less than toLength - /// x x x x - /// ~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooooooooooooooooooooooo------------------- - /// |Offset |fromLength toLength| | - /// - /// Cutting the curve when from Length is greater than toLength - /// x x x x x - /// oooooooooooooooooo--------------------~~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooo - /// | toLength| |Offset |fromLength | - /// - std::vector trim(float fromLength, float toLength, float offsetLength); - - std::vector const &elements() const; - std::vector &mutableElements(); - std::optional const &closed() const; - void setClosed(std::optional const &closed); - std::shared_ptr cgPath() const; - BezierPath copyUsingTransform(Transform2D const &transform) const; - -public: - BezierPath(std::shared_ptr contents); - -public: - std::shared_ptr _contents; -}; - -class BezierPathsBoundingBoxContext { -public: - BezierPathsBoundingBoxContext(); - ~BezierPathsBoundingBoxContext(); - -public: - float *pointsX = nullptr; - float *pointsY = nullptr; - int pointsSize = 0; -}; - -CGRect bezierPathsBoundingBox(std::vector const &paths); -CGRect bezierPathsBoundingBoxParallel(BezierPathsBoundingBoxContext &context, std::vector const &paths); -CGRect bezierPathsBoundingBoxParallel(BezierPathsBoundingBoxContext &context, BezierPath const &path); - -std::vector trimBezierPaths(std::vector &sourcePaths, float start, float end, float offset, TrimType type); - -} - -#endif - -#endif /* BezierPath_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CGPath.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CGPath.h deleted file mode 100644 index 1ddeeb81edf..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CGPath.h +++ /dev/null @@ -1,80 +0,0 @@ -#ifndef CGPath_hpp -#define CGPath_hpp - -#ifdef __cplusplus - -#include - -#include -#include - -namespace lottie { - -struct CGPathItem { - enum class Type { - MoveTo, - LineTo, - CurveTo, - Close - }; - - Type type; - Vector2D points[3] = { Vector2D(0.0, 0.0), Vector2D(0.0, 0.0), Vector2D(0.0, 0.0) }; - - explicit CGPathItem(Type type_) : - type(type_) { - } - - bool operator==(const CGPathItem &rhs) const { - if (type != rhs.type) { - return false; - } - if (points[0] != rhs.points[0]) { - return false; - } - if (points[1] != rhs.points[1]) { - return false; - } - if (points[2] != rhs.points[2]) { - return false; - } - - return true; - } - - bool operator!=(const CGPathItem &rhs) const { - return !(*this == rhs); - } -}; - -class CGPath { -public: - static std::shared_ptr makePath(); - - virtual ~CGPath() = default; - - virtual CGRect boundingBox() const = 0; - - virtual bool empty() const = 0; - - virtual std::shared_ptr copyUsingTransform(Transform2D const &transform) const = 0; - - virtual void addLineTo(Vector2D const &point) = 0; - virtual void addCurveTo(Vector2D const &point, Vector2D const &control1, Vector2D const &control2) = 0; - virtual void moveTo(Vector2D const &point) = 0; - virtual void closeSubpath() = 0; - virtual void addRect(CGRect const &rect) = 0; - virtual void addPath(std::shared_ptr const &path) = 0; - - virtual void enumerate(std::function) = 0; - - virtual bool isEqual(CGPath *other) const = 0; -}; - -Vector2D transformVector(Vector2D const &v, Transform2D const &m); - -} - -#endif - -#endif /* CGPath_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CGPathCocoa.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CGPathCocoa.h deleted file mode 100644 index af32f20bb59..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CGPathCocoa.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef CGPathCocoa_h -#define CGPathCocoa_h - -#ifdef __cplusplus - -#include - -#include -#include - -CGRect calculatePathBoundingBox(CGPathRef path); - -namespace lottie { - -class CGPathCocoaImpl: public CGPath { -public: - CGPathCocoaImpl(); - explicit CGPathCocoaImpl(CGMutablePathRef path); - virtual ~CGPathCocoaImpl(); - - virtual CGRect boundingBox() const override; - - virtual bool empty() const override; - - virtual std::shared_ptr copyUsingTransform(Transform2D const &transform) const override; - - virtual void addLineTo(Vector2D const &point) override; - virtual void addCurveTo(Vector2D const &point, Vector2D const &control1, Vector2D const &control2) override; - virtual void moveTo(Vector2D const &point) override; - virtual void closeSubpath() override; - virtual void addRect(CGRect const &rect) override; - virtual void addPath(std::shared_ptr const &path) override; - virtual CGPathRef nativePath() const; - virtual bool isEqual(CGPath *other) const override; - virtual void enumerate(std::function) override; - - static void withNativePath(std::shared_ptr const &path, std::function f); - -private: - ::CGMutablePathRef _path = nil; -}; - -} - -#endif - -#endif /* CGPathCocoa_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/Color.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/Color.h deleted file mode 100644 index 6e404f29ae9..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/Color.h +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef LottieColor_h -#define LottieColor_h - -#ifdef __cplusplus - -#include -#include - -namespace lottie { - -enum class ColorFormatDenominator { - One, - OneHundred, - TwoFiftyFive -}; - -struct Color { - float r; - float g; - float b; - float a; - - bool operator==(Color const &rhs) const { - if (r != rhs.r) { - return false; - } - if (g != rhs.g) { - return false; - } - if (b != rhs.b) { - return false; - } - if (a != rhs.a) { - return false; - } - return true; - } - - bool operator!=(Color const &rhs) const { - return !(*this == rhs); - } - - explicit Color(float r_, float g_, float b_, float a_, ColorFormatDenominator denominator = ColorFormatDenominator::One); - explicit Color(lottiejson11::Json const &jsonAny) noexcept(false); - - lottiejson11::Json toJson() const; - - static Color fromString(std::string const &string); -}; - -} - -#endif - -#endif /* LottieColor_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CurveVertex.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CurveVertex.h deleted file mode 100644 index 90e3ccf305f..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/CurveVertex.h +++ /dev/null @@ -1,200 +0,0 @@ -#ifndef CurveVertex_h -#define CurveVertex_h - -#ifdef __cplusplus - -#include -#include - -#include - -namespace lottie { - -template -struct CurveVertexSplitResult { - T start; - T trimPoint; - T end; - - explicit CurveVertexSplitResult( - T const &start_, - T const &trimPoint_, - T const &end_ - ) : - start(start_), - trimPoint(trimPoint_), - end(end_) { - } -}; - -/// A single vertex with an in and out tangent -struct __attribute__((packed)) CurveVertex { -private: - /// Initializes a curve point with absolute or relative values - explicit CurveVertex(Vector2D const &point_, Vector2D const &inTangent_, Vector2D const &outTangent_, bool isRelative_) : - point(point_), - inTangent(isRelative_ ? (point_ + inTangent_) : inTangent_), - outTangent(isRelative_ ? (point_ + outTangent_) : outTangent_) { - } - -public: - static CurveVertex absolute(Vector2D const &point_, Vector2D const &inTangent_, Vector2D const &outTangent_) { - return CurveVertex(point_, inTangent_, outTangent_, false); - } - - static CurveVertex relative(Vector2D const &point_, Vector2D const &inTangent_, Vector2D const &outTangent_) { - return CurveVertex(point_, inTangent_, outTangent_, true); - } - - Vector2D inTangentRelative() const { - Vector2D result = inTangent - point; - return result; - } - - Vector2D outTangentRelative() const { - Vector2D result = outTangent - point; - return result; - } - - CurveVertex reversed() const { - return CurveVertex(point, outTangent, inTangent, false); - } - - CurveVertex translated(Vector2D const &translation) const { - return CurveVertex(point + translation, inTangent + translation, outTangent + translation, false); - } - - CurveVertex transformed(Transform2D const &transform) const { - return CurveVertex(transformVector(point, transform), transformVector(inTangent, transform), transformVector(outTangent, transform), false); - } - -public: - Vector2D point = Vector2D::Zero(); - Vector2D inTangent = Vector2D::Zero(); - Vector2D outTangent = Vector2D::Zero(); - - /// Trims a path defined by two Vertices at a specific position, from 0 to 1 - /// - /// The path can be visualized below. - /// - /// F is fromVertex. - /// V is the vertex of the receiver. - /// P is the position from 0-1. - /// O is the outTangent of fromVertex. - /// F====O=========P=======I====V - /// - /// After trimming the curve can be visualized below. - /// - /// S is the returned Start vertex. - /// E is the returned End vertex. - /// T is the trim point. - /// TI and TO are the new tangents for the trimPoint - /// NO and NI are the new tangents for the startPoint and endPoints - /// S==NO=========TI==T==TO=======NI==E - CurveVertexSplitResult splitCurve(CurveVertex const &toVertex, float position) const { - /// If position is less than or equal to 0, trim at start. - if (position <= 0.0) { - return CurveVertexSplitResult( - CurveVertex(point, inTangentRelative(), Vector2D::Zero(), true), - CurveVertex(point, Vector2D::Zero(), outTangentRelative(), true), - toVertex - ); - } - - /// If position is greater than or equal to 1, trim at end. - if (position >= 1.0) { - return CurveVertexSplitResult( - *this, - CurveVertex(toVertex.point, toVertex.inTangentRelative(), Vector2D::Zero(), true), - CurveVertex(toVertex.point, Vector2D::Zero(), toVertex.outTangentRelative(), true) - ); - } - - if (outTangentRelative().isZero() && toVertex.inTangentRelative().isZero()) { - /// If both tangents are zero, then span to be trimmed is a straight line. - Vector2D trimPoint = interpolate(point, toVertex.point, position); - return CurveVertexSplitResult( - *this, - CurveVertex(trimPoint, Vector2D::Zero(), Vector2D::Zero(), true), - toVertex - ); - } - /// Cutting by amount gives incorrect length.... - /// One option is to cut by a stride until it gets close then edge it down. - /// Measuring a percentage of the spans does not equal the same as measuring a percentage of length. - /// This is where the historical trim path bugs come from. - Vector2D a = interpolate(point, outTangent, position); - Vector2D b = interpolate(outTangent, toVertex.inTangent, position); - Vector2D c = interpolate(toVertex.inTangent, toVertex.point, position); - Vector2D d = interpolate(a, b, position); - Vector2D e = interpolate(b, c, position); - Vector2D f = interpolate(d, e, position); - return CurveVertexSplitResult( - CurveVertex::absolute(point, inTangent, a), - CurveVertex::absolute(f, d, e), - CurveVertex::absolute(toVertex.point, c, toVertex.outTangent) - ); - } - - /// Trims a curve of a known length to a specific length and returns the points. - /// - /// There is not a performant yet accurate way to cut a curve to a specific length. - /// This calls splitCurve(toVertex: position:) to split the curve and then measures - /// the length of the new curve. The function then iterates through the samples, - /// adjusting the position of the cut for a more precise cut. - /// Usually a single iteration is enough to get within 0.5 points of the desired - /// length. - /// - /// This function should probably live in PathElement, since it deals with curve - /// lengths. - CurveVertexSplitResult trimCurve(CurveVertex const &toVertex, float atLength, float curveLength, int maxSamples, float accuracy = 1.0f) const { - float currentPosition = atLength / curveLength; - auto results = splitCurve(toVertex, currentPosition); - - if (maxSamples == 0) { - return results; - } - - for (int i = 1; i <= maxSamples; i++) { - auto length = results.start.distanceTo(results.trimPoint); - auto lengthDiff = atLength - length; - /// Check if length is correct. - if (lengthDiff < accuracy) { - return results; - } - auto diffPosition = std::max(std::min((currentPosition / length) * lengthDiff, currentPosition * 0.5f), currentPosition * (-0.5f)); - currentPosition = diffPosition + currentPosition; - results = splitCurve(toVertex, currentPosition); - } - return results; - } - - /// The distance from the receiver to the provided vertex. - /// - /// For lines (zeroed tangents) the distance between the two points is measured. - /// For curves the curve is iterated over by sample count and the points are measured. - /// This is ~99% accurate at a sample count of 30 - float distanceTo(CurveVertex const &toVertex, int sampleCount = 25) const { - if (outTangentRelative().isZero() && toVertex.inTangentRelative().isZero()) { - /// Return a linear distance. - return point.distanceTo(toVertex.point); - } - - float distance = 0.0; - - auto previousPoint = point; - for (int i = 0; i < sampleCount; i++) { - auto pointOnCurve = splitCurve(toVertex, ((float)(i)) / ((float)(sampleCount))).trimPoint; - distance = distance + previousPoint.distanceTo(pointOnCurve.point); - previousPoint = pointOnCurve.point; - } - distance = distance + previousPoint.distanceTo(toVertex.point); - return distance; - } -}; - -} - -#endif - -#endif /* CurveVertex_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimation.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimation.h deleted file mode 100644 index e7112776ede..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimation.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef LottieAnimation_h -#define LottieAnimation_h - -#import - -#import "LottieRenderTree.h" - -#ifdef __cplusplus -extern "C" { -#endif - -@interface LottieAnimation : NSObject - -@property (nonatomic, readonly) NSInteger frameCount; -@property (nonatomic, readonly) NSInteger framesPerSecond; -@property (nonatomic, readonly) CGSize size; - -- (instancetype _Nullable)initWithData:(NSData * _Nonnull)data; - -- (NSData * _Nonnull)toJson; - -@end - -#ifdef __cplusplus -} -#endif - -#endif /* LottieAnimation_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimationContainer.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimationContainer.h deleted file mode 100644 index f4e936e77da..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimationContainer.h +++ /dev/null @@ -1,71 +0,0 @@ -#ifndef LottieAnimationContainer_h -#define LottieAnimationContainer_h - -#ifdef __cplusplus - -#import "LottieAnimation.h" -#import "LottieRenderTree.h" -#import "LottieAnimationContainer.h" - -#include - -namespace lottie { - -class RenderTreeNode; - -} - -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct { - CGRect bounds; - CGPoint position; - CATransform3D transform; - float opacity; - bool masksToBounds; - bool isHidden; -} LottieRenderNodeLayerData; - -typedef struct { - int64_t internalId; - bool isValid; - LottieRenderNodeLayerData layer; - CGRect globalRect; - CGRect localRect; - CATransform3D globalTransform; - bool drawsContent; - bool hasSimpleContents; - int drawContentDescendants; - bool isInvertedMatte; - int64_t maskId; - int subnodeCount; -} LottieRenderNodeProxy; - -@interface LottieAnimationContainer : NSObject - -@property (nonatomic, strong, readonly) LottieAnimation * _Nonnull animation; - -- (instancetype _Nonnull)initWithAnimation:(LottieAnimation * _Nonnull)animation; - -- (void)update:(NSInteger)frame; -- (LottieRenderNode * _Nullable)getCurrentRenderTreeForSize:(CGSize)size; - -#ifdef __cplusplus -- (std::shared_ptr)internalGetRootRenderTreeNode; -#endif - -- (int64_t)getRootRenderNodeProxy; -- (LottieRenderNodeProxy)getRenderNodeProxyById:(int64_t)nodeId __attribute__((objc_direct)); -- (LottieRenderNodeProxy)getRenderNodeSubnodeProxyById:(int64_t)nodeId index:(int)index __attribute__((objc_direct)); - -@end - -#ifdef __cplusplus -} -#endif - -#endif /* LottieAnimationContainer_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieCpp.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieCpp.h deleted file mode 100644 index 22091e064b4..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieCpp.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef LottieCpp_h -#define LottieCpp_h - -#import - -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import - -#endif /* LottieCpp_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieRenderTree.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieRenderTree.h deleted file mode 100644 index e4158c3ccbe..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieRenderTree.h +++ /dev/null @@ -1,147 +0,0 @@ -#ifndef LottieRenderTree_h -#define LottieRenderTree_h - -#import - -#ifdef __cplusplus -extern "C" { -#endif - -typedef NS_ENUM(NSUInteger, LottiePathItemType) { - LottiePathItemTypeMoveTo, - LottiePathItemTypeLineTo, - LottiePathItemTypeCurveTo, - LottiePathItemTypeClose -}; - -typedef struct { - LottiePathItemType type; - CGPoint points[4]; -} LottiePathItem; - -typedef struct { - CGFloat r; - CGFloat g; - CGFloat b; - CGFloat a; -} LottieColor; - -typedef NS_ENUM(NSUInteger, LottieFillRule) { - LottieFillRuleEvenOdd, - LottieFillRuleWinding -}; - -typedef NS_ENUM(NSUInteger, LottieGradientType) { - LottieGradientTypeLinear, - LottieGradientTypeRadial -}; - -@interface LottieColorStop : NSObject - -@property (nonatomic, readonly, direct) LottieColor color; -@property (nonatomic, readonly, direct) CGFloat location; - -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithColor:(LottieColor)color location:(CGFloat)location __attribute__((objc_direct)); - -@end - -@interface LottiePath : NSObject - -- (void)enumerateItems:(void (^ _Nonnull)(LottiePathItem * _Nonnull))iterate __attribute__((objc_direct)); - -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithCustomData:(NSData * _Nonnull)customData __attribute__((objc_direct)); - -@end - -@interface LottieRenderContentShading : NSObject - -@end - -@interface LottieRenderContentSolidShading : LottieRenderContentShading - -@property (nonatomic, readonly, direct) LottieColor color; -@property (nonatomic, readonly, direct) CGFloat opacity; - -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithColor:(LottieColor)color opacity:(CGFloat)opacity __attribute__((objc_direct)); - -@end - -@interface LottieRenderContentGradientShading : LottieRenderContentShading - -@property (nonatomic, readonly, direct) CGFloat opacity; -@property (nonatomic, readonly, direct) LottieGradientType gradientType; -@property (nonatomic, strong, readonly, direct) NSArray * _Nonnull colorStops; -@property (nonatomic, readonly, direct) CGPoint start; -@property (nonatomic, readonly, direct) CGPoint end; - -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithOpacity:(CGFloat)opacity gradientType:(LottieGradientType)gradientType colorStops:(NSArray * _Nonnull)colorStops start:(CGPoint)start end:(CGPoint)end __attribute__((objc_direct)); - -@end - -@interface LottieRenderContentFill : NSObject - -@property (nonatomic, strong, readonly, direct) LottieRenderContentShading * _Nonnull shading; -@property (nonatomic, readonly, direct) LottieFillRule fillRule; - -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading fillRule:(LottieFillRule)fillRule __attribute__((objc_direct)); - -@end - -@interface LottieRenderContentStroke : NSObject - -@property (nonatomic, strong, readonly, direct) LottieRenderContentShading * _Nonnull shading; -@property (nonatomic, readonly, direct) CGFloat lineWidth; -@property (nonatomic, readonly, direct) CGLineJoin lineJoin; -@property (nonatomic, readonly, direct) CGLineCap lineCap; -@property (nonatomic, readonly, direct) CGFloat miterLimit; -@property (nonatomic, readonly, direct) CGFloat dashPhase; -@property (nonatomic, strong, readonly, direct) NSArray * _Nullable dashPattern; - -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading lineWidth:(CGFloat)lineWidth lineJoin:(CGLineJoin)lineJoin lineCap:(CGLineCap)lineCap miterLimit:(CGFloat)miterLimit dashPhase:(CGFloat)dashPhase dashPattern:(NSArray * _Nullable)dashPattern __attribute__((objc_direct)); - -@end - -@interface LottieRenderContent : NSObject - -@property (nonatomic, strong, readonly, direct) LottiePath * _Nonnull path; -@property (nonatomic, strong, readonly, direct) LottieRenderContentStroke * _Nullable stroke; -@property (nonatomic, strong, readonly, direct) LottieRenderContentFill * _Nullable fill; - -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithPath:(LottiePath * _Nonnull)path stroke:(LottieRenderContentStroke * _Nullable)stroke fill:(LottieRenderContentFill * _Nullable)fill __attribute__((objc_direct)); - -@end - -@interface LottieRenderNode : NSObject - -@property (nonatomic, readonly, direct) CGPoint position; -@property (nonatomic, readonly, direct) CGRect bounds; -@property (nonatomic, readonly, direct) CATransform3D transform; -@property (nonatomic, readonly, direct) CGFloat opacity; -@property (nonatomic, readonly, direct) bool masksToBounds; -@property (nonatomic, readonly, direct) bool isHidden; - -@property (nonatomic, readonly, direct) CGRect globalRect; -@property (nonatomic, readonly, direct) CATransform3D globalTransform; -@property (nonatomic, readonly, direct) LottieRenderContent * _Nullable renderContent; -@property (nonatomic, readonly, direct) bool hasSimpleContents; -@property (nonatomic, readonly, direct) bool isInvertedMatte; -@property (nonatomic, readonly, direct) NSArray * _Nonnull subnodes; -@property (nonatomic, readonly, direct) LottieRenderNode * _Nullable mask; - -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithPosition:(CGPoint)position bounds:(CGRect)bounds transform:(CATransform3D)transform opacity:(CGFloat)opacity masksToBounds:(bool)masksToBounds isHidden:(bool)isHidden globalRect:(CGRect)globalRect globalTransform:(CATransform3D)globalTransform renderContent:(LottieRenderContent * _Nullable)renderContent hasSimpleContents:(bool)hasSimpleContents isInvertedMatte:(bool)isInvertedMatte subnodes:(NSArray * _Nonnull)subnodes mask:(LottieRenderNode * _Nullable)mask __attribute__((objc_direct)); - -@end - -#ifdef __cplusplus -} -#endif - -#endif /* LottieRenderTree_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/PathElement.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/PathElement.h deleted file mode 100644 index bb8a8f578d4..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/PathElement.h +++ /dev/null @@ -1,94 +0,0 @@ -#ifndef PathElement_h -#define PathElement_h - -#ifdef __cplusplus - -#include - -namespace lottie { - -template -struct PathSplitResultSpan { - T start; - T end; - - explicit PathSplitResultSpan(T const &start_, T const &end_) : - start(start_), end(end_) { - } -}; - -template -struct PathSplitResult { - PathSplitResultSpan leftSpan; - PathSplitResultSpan rightSpan; - - explicit PathSplitResult(PathSplitResultSpan const &leftSpan_, PathSplitResultSpan const &rightSpan_) : - leftSpan(leftSpan_), rightSpan(rightSpan_) { - } -}; - -/// A path section, containing one point and its length to the previous point. -/// -/// The relationship between this path element and the previous is implicit. -/// Ideally a path section would be defined by two vertices and a length. -/// We don't do this however, as it would effectively double the memory footprint -/// of path data. -/// -struct __attribute__((packed)) PathElement { - /// Initializes a new path with length of 0 - explicit PathElement(CurveVertex const &vertex_) : - vertex(vertex_) { - } - - /// Initializes a new path with length - explicit PathElement(std::optional length_, CurveVertex const &vertex_) : - vertex(vertex_) { - } - - /// The vertex of the element - CurveVertex vertex; - - /// Returns a new path element define the span from the receiver to the new vertex. - PathElement pathElementTo(CurveVertex const &toVertex) const { - return PathElement(std::nullopt, toVertex); - } - - PathElement updateVertex(CurveVertex const &newVertex) const { - return PathElement(newVertex); - } - - /// Splits an element span defined by the receiver and fromElement to a position 0-1 - PathSplitResult splitElementAtPosition(PathElement const &fromElement, float atLength) { - /// Trim the span. Start and trim go into the first, trim and end go into second. - auto trimResults = fromElement.vertex.trimCurve(vertex, atLength, length(fromElement), 3); - - /// Create the elements for the break - auto spanAStart = PathElement( - std::nullopt, - CurveVertex::absolute( - fromElement.vertex.point, - fromElement.vertex.inTangent, - trimResults.start.outTangent - )); - /// Recalculating the length here is a waste as the trimCurve function also accurately calculates this length. - auto spanAEnd = spanAStart.pathElementTo(trimResults.trimPoint); - - auto spanBStart = PathElement(trimResults.trimPoint); - auto spanBEnd = spanBStart.pathElementTo(trimResults.end); - return PathSplitResult( - PathSplitResultSpan(spanAStart, spanAEnd), - PathSplitResultSpan(spanBStart, spanBEnd) - ); - } - - float length(PathElement const &previous) { - float result = previous.vertex.distanceTo(vertex); - return result; - } -}; - -} - -#endif - -#endif /* PathElement_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h deleted file mode 100644 index 09616bdf0a8..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h +++ /dev/null @@ -1,471 +0,0 @@ -#ifndef RenderTreeNode_hpp -#define RenderTreeNode_hpp - -#ifdef __cplusplus - -#include -#include -#include -#include -#include - -#include - -namespace lottie { - -class ProcessedRenderTreeNodeData { -public: - ProcessedRenderTreeNodeData() { - } - - bool isValid = false; - bool isInvertedMatte = false; -}; - -class RenderableItem { -public: - enum class Type { - Shape, - GradientFill - }; - -public: - RenderableItem() { - } - - virtual ~RenderableItem() = default; - - virtual Type type() const = 0; - virtual CGRect boundingRect() const = 0; - - virtual bool isEqual(std::shared_ptr rhs) const = 0; -}; - -class ShapeRenderableItem: public RenderableItem { -public: - struct Fill { - Color color; - FillRule rule; - - Fill(Color color_, FillRule rule_) : - color(color_), rule(rule_) { - } - - bool operator==(Fill const &rhs) const { - if (color != rhs.color) { - return false; - } - if (rule != rhs.rule) { - return false; - } - return true; - } - - bool operator!=(Fill const &rhs) const { - return !(*this == rhs); - } - }; - - struct Stroke { - Color color; - float lineWidth = 0.0; - LineJoin lineJoin = LineJoin::Round; - LineCap lineCap = LineCap::Square; - float dashPhase = 0.0; - std::vector dashPattern; - - Stroke( - Color color_, - float lineWidth_, - LineJoin lineJoin_, - LineCap lineCap_, - float dashPhase_, - std::vector dashPattern_ - ) : - color(color_), - lineWidth(lineWidth_), - lineJoin(lineJoin_), - lineCap(lineCap_), - dashPhase(dashPhase_), - dashPattern(dashPattern_) { - } - - bool operator==(Stroke const &rhs) const { - if (color != rhs.color) { - return false; - } - if (lineWidth != rhs.lineWidth) { - return false; - } - if (lineJoin != rhs.lineJoin) { - return false; - } - if (lineCap != rhs.lineCap) { - return false; - } - if (dashPhase != rhs.dashPhase) { - return false; - } - if (dashPattern != rhs.dashPattern) { - return false; - } - return true; - } - - bool operator!=(Stroke const &rhs) const { - return !(*this == rhs); - } - }; - -public: - ShapeRenderableItem( - std::shared_ptr path_, - std::optional const &fill_, - std::optional const &stroke_ - ) : - path(path_), - fill(fill_), - stroke(stroke_) { - } - - virtual Type type() const override { - return Type::Shape; - } - - virtual CGRect boundingRect() const override { - if (path) { - CGRect shapeBounds = path->boundingBox(); - if (stroke) { - shapeBounds = shapeBounds.insetBy(-stroke->lineWidth / 2.0, -stroke->lineWidth / 2.0); - } - return shapeBounds; - } else { - return CGRect(0.0, 0.0, 0.0, 0.0); - } - } - - virtual bool isEqual(std::shared_ptr rhs) const override { - if (rhs->type() != type()) { - return false; - } - ShapeRenderableItem *other = (ShapeRenderableItem *)rhs.get(); - if ((path == nullptr) != (other->path == nullptr)) { - return false; - } else if (path) { - if (!path->isEqual(other->path.get())) { - return false; - } - } - if (fill != other->fill) { - return false; - } - if (stroke != other->stroke) { - return false; - } - return false; - } - -public: - std::shared_ptr path; - std::optional fill; - std::optional stroke; -}; - -class GradientFillRenderableItem: public RenderableItem { -public: - GradientFillRenderableItem( - std::shared_ptr path_, - FillRule pathFillRule_, - GradientType gradientType_, - std::vector const &colors_, - std::vector const &locations_, - Vector2D const &start_, - Vector2D const &end_, - CGRect bounds_ - ) : - path(path_), - pathFillRule(pathFillRule_), - gradientType(gradientType_), - colors(colors_), - locations(locations_), - start(start_), - end(end_), - bounds(bounds_) { - } - - virtual Type type() const override { - return Type::GradientFill; - } - - virtual CGRect boundingRect() const override { - return bounds; - } - - virtual bool isEqual(std::shared_ptr rhs) const override { - if (rhs->type() != type()) { - return false; - } - GradientFillRenderableItem *other = (GradientFillRenderableItem *)rhs.get(); - - if (gradientType != other->gradientType) { - return false; - } - if (colors != other->colors) { - return false; - } - if (locations != other->locations) { - return false; - } - if (start != other->start) { - return false; - } - if (end != other->end) { - return false; - } - if (bounds != other->bounds) { - return false; - } - - return true; - } - -public: - std::shared_ptr path; - FillRule pathFillRule; - GradientType gradientType; - std::vector colors; - std::vector locations; - Vector2D start; - Vector2D end; - CGRect bounds; -}; - -class RenderTreeNodeContentShadingVariant; - -struct RenderTreeNodeContentPath { -public: - explicit RenderTreeNodeContentPath(BezierPath path_) : - path(path_) { - } - - BezierPath path; - CGRect bounds = CGRect(0.0, 0.0, 0.0, 0.0); - bool needsBoundsRecalculation = true; -}; - -class RenderTreeNodeContentItem { -public: - enum class ShadingType { - Solid, - Gradient - }; - - class Shading { - public: - Shading() { - } - - virtual ~Shading() = default; - - virtual ShadingType type() const = 0; - }; - - class SolidShading: public Shading { - public: - SolidShading(Color const &color_, float opacity_) : - color(color_), - opacity(opacity_) { - } - - virtual ShadingType type() const override { - return ShadingType::Solid; - } - - public: - Color color; - float opacity = 0.0; - }; - - class GradientShading: public Shading { - public: - GradientShading( - float opacity_, - GradientType gradientType_, - std::vector const &colors_, - std::vector const &locations_, - Vector2D const &start_, - Vector2D const &end_ - ) : - opacity(opacity_), - gradientType(gradientType_), - colors(colors_), - locations(locations_), - start(start_), - end(end_) { - } - - virtual ShadingType type() const override { - return ShadingType::Gradient; - } - - public: - float opacity = 0.0; - GradientType gradientType; - std::vector colors; - std::vector locations; - Vector2D start; - Vector2D end; - }; - - struct Stroke { - std::shared_ptr shading; - float lineWidth = 0.0; - LineJoin lineJoin = LineJoin::Round; - LineCap lineCap = LineCap::Square; - float miterLimit = 4.0; - float dashPhase = 0.0; - std::vector dashPattern; - - Stroke( - std::shared_ptr shading_, - float lineWidth_, - LineJoin lineJoin_, - LineCap lineCap_, - float miterLimit_, - float dashPhase_, - std::vector dashPattern_ - ) : - shading(shading_), - lineWidth(lineWidth_), - lineJoin(lineJoin_), - lineCap(lineCap_), - miterLimit(miterLimit_), - dashPhase(dashPhase_), - dashPattern(dashPattern_) { - } - }; - - struct Fill { - std::shared_ptr shading; - FillRule rule; - - Fill( - std::shared_ptr shading_, - FillRule rule_ - ) : - shading(shading_), - rule(rule_) { - } - }; - -public: - RenderTreeNodeContentItem() { - } - -public: - bool isGroup = false; - Transform2D transform = Transform2D::identity(); - float alpha = 0.0; - std::optional trimParams; - std::shared_ptr path; - std::vector> shadings; - std::vector> subItems; - int drawContentCount = 0; - - ProcessedRenderTreeNodeData renderData; -}; - -class RenderTreeNodeContentShadingVariant { -public: - RenderTreeNodeContentShadingVariant() { - } - -public: - std::shared_ptr stroke; - std::shared_ptr fill; - std::optional> explicitPath; - - size_t subItemLimit = 0; -}; - -class RenderTreeNode { -public: - RenderTreeNode( - Vector2D size_, - Transform2D transform_, - float alpha_, - bool masksToBounds_, - bool isHidden_, - std::vector> subnodes_, - std::shared_ptr mask_, - bool invertMask_ - ) : - _size(size_), - _transform(transform_), - _alpha(alpha_), - _masksToBounds(masksToBounds_), - _isHidden(isHidden_), - _subnodes(subnodes_), - _mask(mask_), - _invertMask(invertMask_) { - for (const auto &subnode : _subnodes) { - drawContentCount += subnode->drawContentCount; - } - } - - ~RenderTreeNode() { - } - -public: - Vector2D const &size() const { - return _size; - } - - Transform2D const &transform() const { - return _transform; - } - - float alpha() const { - return _alpha; - } - - bool masksToBounds() const { - return _masksToBounds; - } - - bool isHidden() const { - return _isHidden; - } - - std::vector> const &subnodes() const { - return _subnodes; - } - - std::shared_ptr const &mask() const { - return _mask; - } - - bool invertMask() const { - return _invertMask; - } - -public: - Vector2D _size; - Transform2D _transform = Transform2D::identity(); - float _alpha = 1.0f; - bool _masksToBounds = false; - bool _isHidden = false; - std::shared_ptr _contentItem; - int drawContentCount = 0; - std::vector> _subnodes; - std::shared_ptr _mask; - bool _invertMask = false; - - ProcessedRenderTreeNodeData renderData; -}; - -} - -#endif - -#endif /* RenderTreeNode_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/ShapeAttributes.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/ShapeAttributes.h deleted file mode 100644 index c3c6f9d809a..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/ShapeAttributes.h +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef ShapeAttributes_h -#define ShapeAttributes_h - -#ifdef __cplusplus - -namespace lottie { - -enum class FillRule: int { - None = 0, - NonZeroWinding = 1, - EvenOdd = 2 -}; - -enum class LineCap: int { - None = 0, - Butt = 1, - Round = 2, - Square = 3 -}; - -enum class LineJoin: int { - None = 0, - Miter = 1, - Round = 2, - Bevel = 3 -}; - -enum class GradientType: int { - None = 0, - Linear = 1, - Radial = 2 -}; - -enum class TrimType: int { - Simultaneously = 1, - Individually = 2 -}; - -struct TrimParams { - float start = 0.0; - float end = 0.0; - float offset = 0.0; - TrimType type = TrimType::Simultaneously; - - TrimParams(float start_, float end_, float offset_, TrimType type_) : - start(start_), - end(end_), - offset(offset_), - type(type_) { - } -}; - -} - -#endif - -#endif /* LottieColor_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/Vectors.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/Vectors.h deleted file mode 100644 index ffe55968720..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/Vectors.h +++ /dev/null @@ -1,278 +0,0 @@ -#ifndef Vectors_hpp -#define Vectors_hpp - -#ifdef __cplusplus - -#include -#include - -#include - -#import - -namespace lottie { - -struct Vector1D { - enum class InternalRepresentationType { - SingleNumber, - Array - }; - - explicit Vector1D(float value_) : - value(value_) { - } - - explicit Vector1D(lottiejson11::Json const &json) noexcept(false); - lottiejson11::Json toJson() const; - - float value; - - float distanceTo(Vector1D const &to) const { - return abs(to.value - value); - } -}; - -float interpolate(float value, float to, float amount); - -Vector1D interpolate( - Vector1D const &from, - Vector1D const &to, - float amount -); - -struct __attribute__((packed)) Vector2D { - static Vector2D Zero() { - return Vector2D(0.0, 0.0); - } - - Vector2D() : - x(0.0), - y(0.0) { - } - - explicit Vector2D(float x_, float y_) : - x(x_), - y(y_) { - } - - explicit Vector2D(lottiejson11::Json const &json) noexcept(false); - lottiejson11::Json toJson() const; - - float x; - float y; - - Vector2D operator+(Vector2D const &rhs) const { - return Vector2D(x + rhs.x, y + rhs.y); - } - - Vector2D operator-(Vector2D const &rhs) const { - return Vector2D(x - rhs.x, y - rhs.y); - } - - Vector2D operator*(float scalar) const { - return Vector2D(x * scalar, y * scalar); - } - - bool operator==(Vector2D const &rhs) const { - return x == rhs.x && y == rhs.y; - } - - bool operator!=(Vector2D const &rhs) const { - return !(*this == rhs); - } - - bool isZero() const { - return x == 0.0 && y == 0.0; - } - - float distanceTo(Vector2D const &to) const { - auto deltaX = to.x - x; - auto deltaY = to.y - y; - return sqrt(deltaX * deltaX + deltaY * deltaY); - } - - bool colinear(Vector2D const &a, Vector2D const &b) const { - float area = x * (a.y - b.y) + a.x * (b.y - y) + b.x * (y - a.y); - float accuracy = 0.05; - if (area < accuracy && area > -accuracy) { - return true; - } - return false; - } - - Vector2D pointOnPath(Vector2D const &to, Vector2D const &outTangent, Vector2D const &inTangent, float amount) const; - - Vector2D interpolate(Vector2D const &to, float amount) const; - - Vector2D interpolate( - Vector2D const &to, - Vector2D const &outTangent, - Vector2D const &inTangent, - float amount, - int maxIterations = 3, - int samples = 20, - float accuracy = 1.0 - ) const; -}; - -Vector2D interpolate( - Vector2D const &from, - Vector2D const &to, - float amount -); - -struct Vector3D { - explicit Vector3D(float x_, float y_, float z_) : - x(x_), - y(y_), - z(z_) { - } - - explicit Vector3D(lottiejson11::Json const &json) noexcept(false); - lottiejson11::Json toJson() const; - - float x = 0.0; - float y = 0.0; - float z = 0.0; -}; - -Vector3D interpolate( - Vector3D const &from, - Vector3D const &to, - float amount -); - -inline float degreesToRadians(float value) { - return value * M_PI / 180.0f; -} - -inline float radiansToDegrees(float value) { - return value * 180.0f / M_PI; -} - -struct Transform2D { - static Transform2D const &identity() { - return _identity; - } - - explicit Transform2D(simd_float3x3 const &rows_) : - _rows(rows_) { - } - - Transform2D operator*(Transform2D const &other) const { - return Transform2D(simd_mul(other._rows, _rows)); - } - - bool isInvertible() const { - return simd_determinant(_rows) > 0.00000001; - } - - Transform2D inverted() const { - return Transform2D(simd_inverse(_rows)); - } - - bool isIdentity() const { - return (*this) == identity(); - } - - static Transform2D makeTranslation(float tx, float ty); - static Transform2D makeScale(float sx, float sy); - static Transform2D makeRotation(float radians); - static Transform2D makeSkew(float skew, float skewAxis); - static Transform2D makeTransform( - Vector2D const &anchor, - Vector2D const &position, - Vector2D const &scale, - float rotation, - std::optional skew, - std::optional skewAxis - ); - - Transform2D rotated(float degrees) const; - Transform2D translated(Vector2D const &translation) const; - Transform2D scaled(Vector2D const &scale) const; - Transform2D skewed(float skew, float skewAxis) const; - - bool operator==(Transform2D const &rhs) const { - return simd_equal(_rows, rhs._rows); - } - - bool operator!=(Transform2D const &rhs) const { - return !((*this) == rhs); - } - - simd_float3x3 const &rows() const { - return _rows; - } -private: - static Transform2D _identity; - - simd_float3x3 _rows; -}; - -struct CGRect { - explicit CGRect(float x_, float y_, float width_, float height_) : - x(x_), y(y_), width(width_), height(height_) { - } - - float x = 0.0f; - float y = 0.0f; - float width = 0.0f; - float height = 0.0f; - - static CGRect veryLarge() { - return CGRect( - -100000000.0f, - -100000000.0f, - 200000000.0f, - 200000000.0f - ); - } - - bool operator==(CGRect const &rhs) const { - return x == rhs.x && y == rhs.y && width == rhs.width && height == rhs.height; - } - - bool operator!=(CGRect const &rhs) const { - return !(*this == rhs); - } - - bool empty() const { - return width <= 0.0 || height <= 0.0; - } - - CGRect insetBy(float dx, float dy) const { - CGRect result = *this; - - result.x += dx; - result.y += dy; - result.width -= dx * 2.0f; - result.height -= dy * 2.0f; - - return result; - } - - bool intersects(CGRect const &other) const; - bool contains(CGRect const &other) const; - - CGRect intersection(CGRect const &other) const; - CGRect unionWith(CGRect const &other) const; - - CGRect applyingTransform(Transform2D const &transform) const; -}; - -inline bool isInRangeOrEqual(float value, float from, float to) { - return from <= value && value <= to; -} - -inline bool isInRange(float value, float from, float to) { - return from < value && value < to; -} - -float cubicBezierInterpolate(float value, Vector2D const &P0, Vector2D const &P1, Vector2D const &P2, Vector2D const &P3); - -} - -#endif - -#endif /* Vectors_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/VectorsCocoa.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/VectorsCocoa.h deleted file mode 100644 index ef77ad99668..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/VectorsCocoa.h +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef VectorsCocoa_h -#define VectorsCocoa_h - -#ifdef __cplusplus - -#import - -namespace lottie { - -::CATransform3D nativeTransform(Transform2D const &value); -Transform2D fromNativeTransform(::CATransform3D const &value); - -} - -#endif - -#endif /* VectorsCocoa_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/lottiejson11.hpp b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/lottiejson11.hpp deleted file mode 100644 index aa188f366b1..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/lottiejson11.hpp +++ /dev/null @@ -1,236 +0,0 @@ -/* json11 - * - * json11 is a tiny JSON library for C++11, providing JSON parsing and serialization. - * - * The core object provided by the library is json11::Json. A Json object represents any JSON - * value: null, bool, number (int or double), string (std::string), array (std::vector), or - * object (std::map). - * - * Json objects act like values: they can be assigned, copied, moved, compared for equality or - * order, etc. There are also helper methods Json::dump, to serialize a Json to a string, and - * Json::parse (static) to parse a std::string as a Json object. - * - * Internally, the various types of Json object are represented by the JsonValue class - * hierarchy. - * - * A note on numbers - JSON specifies the syntax of number formatting but not its semantics, - * so some JSON implementations distinguish between integers and floating-point numbers, while - * some don't. In json11, we choose the latter. Because some JSON implementations (namely - * Javascript itself) treat all numbers as the same type, distinguishing the two leads - * to JSON that will be *silently* changed by a round-trip through those implementations. - * Dangerous! To avoid that risk, json11 stores all numbers as double internally, but also - * provides integer helpers. - * - * Fortunately, double-precision IEEE754 ('double') can precisely store any integer in the - * range +/-2^53, which includes every 'int' on most systems. (Timestamps often use int64 - * or long long to avoid the Y2038K problem; a double storing microseconds since some epoch - * will be exact for +/- 275 years.) - */ - -/* Copyright (c) 2013 Dropbox, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -#pragma once - -#ifdef __cplusplus - -#include -#include -#include -#include -#include - -#ifdef _MSC_VER - #if _MSC_VER <= 1800 // VS 2013 - #ifndef noexcept - #define noexcept throw() - #endif - - #ifndef snprintf - #define snprintf _snprintf_s - #endif - #endif -#endif - -namespace lottiejson11 { - -enum JsonParse { - STANDARD, COMMENTS -}; - -class JsonValue; - -class Json final { -public: - // Types - enum Type { - NUL, NUMBER, BOOL, STRING, ARRAY, OBJECT - }; - - // Array and object typedefs - typedef std::vector array; - typedef std::map object; - - // Constructors for the various types of JSON value. - Json() noexcept; // NUL - Json(std::nullptr_t) noexcept; // NUL - Json(double value); // NUMBER - Json(int value); // NUMBER - Json(bool value); // BOOL - Json(const std::string &value); // STRING - Json(std::string &&value); // STRING - Json(const char * value); // STRING - Json(const array &values); // ARRAY - Json(array &&values); // ARRAY - Json(const object &values); // OBJECT - Json(object &&values); // OBJECT - - // Implicit constructor: anything with a to_json() function. - template - Json(const T & t) : Json(t.to_json()) {} - - // Implicit constructor: map-like objects (std::map, std::unordered_map, etc) - template ().begin()->first)>::value - && std::is_constructible().begin()->second)>::value, - int>::type = 0> - Json(const M & m) : Json(object(m.begin(), m.end())) {} - - // Implicit constructor: vector-like objects (std::list, std::vector, std::set, etc) - template ().begin())>::value, - int>::type = 0> - Json(const V & v) : Json(array(v.begin(), v.end())) {} - - // This prevents Json(some_pointer) from accidentally producing a bool. Use - // Json(bool(some_pointer)) if that behavior is desired. - Json(void *) = delete; - - // Accessors - Type type() const; - - bool is_null() const { return type() == NUL; } - bool is_number() const { return type() == NUMBER; } - bool is_bool() const { return type() == BOOL; } - bool is_string() const { return type() == STRING; } - bool is_array() const { return type() == ARRAY; } - bool is_object() const { return type() == OBJECT; } - - // Return the enclosed value if this is a number, 0 otherwise. Note that json11 does not - // distinguish between integer and non-integer numbers - number_value() and int_value() - // can both be applied to a NUMBER-typed object. - double number_value() const; - int int_value() const; - - // Return the enclosed value if this is a boolean, false otherwise. - bool bool_value() const; - // Return the enclosed string if this is a string, "" otherwise. - const std::string &string_value() const; - // Return the enclosed std::vector if this is an array, or an empty vector otherwise. - const array &array_items() const; - // Return the enclosed std::map if this is an object, or an empty map otherwise. - const object &object_items() const; - - // Return a reference to arr[i] if this is an array, Json() otherwise. - const Json & operator[](size_t i) const; - // Return a reference to obj[key] if this is an object, Json() otherwise. - const Json & operator[](const std::string &key) const; - - // Serialize. - void dump(std::string &out) const; - std::string dump() const { - std::string out; - dump(out); - return out; - } - - // Parse. If parse fails, return Json() and assign an error message to err. - static Json parse(const std::string & in, - std::string & err, - JsonParse strategy = JsonParse::STANDARD); - static Json parse(const char * in, - std::string & err, - JsonParse strategy = JsonParse::STANDARD) { - if (in) { - return parse(std::string(in), err, strategy); - } else { - err = "null input"; - return nullptr; - } - } - // Parse multiple objects, concatenated or separated by whitespace - static std::vector parse_multi( - const std::string & in, - std::string::size_type & parser_stop_pos, - std::string & err, - JsonParse strategy = JsonParse::STANDARD); - - static inline std::vector parse_multi( - const std::string & in, - std::string & err, - JsonParse strategy = JsonParse::STANDARD) { - std::string::size_type parser_stop_pos; - return parse_multi(in, parser_stop_pos, err, strategy); - } - - bool operator== (const Json &rhs) const; - bool operator< (const Json &rhs) const; - bool operator!= (const Json &rhs) const { return !(*this == rhs); } - bool operator<= (const Json &rhs) const { return !(rhs < *this); } - bool operator> (const Json &rhs) const { return (rhs < *this); } - bool operator>= (const Json &rhs) const { return !(*this < rhs); } - - /* has_shape(types, err) - * - * Return true if this is a JSON object and, for each item in types, has a field of - * the given type. If not, return false and set err to a descriptive message. - */ - typedef std::initializer_list> shape; - bool has_shape(const shape & types, std::string & err) const; - -private: - std::shared_ptr m_ptr; -}; - -// Internal class hierarchy - JsonValue objects are not exposed to users of this API. -class JsonValue { -protected: - friend class Json; - friend class JsonInt; - friend class JsonDouble; - virtual Json::Type type() const = 0; - virtual bool equals(const JsonValue * other) const = 0; - virtual bool less(const JsonValue * other) const = 0; - virtual void dump(std::string &out) const = 0; - virtual double number_value() const; - virtual int int_value() const; - virtual bool bool_value() const; - virtual const std::string &string_value() const; - virtual const Json::array &array_items() const; - virtual const Json &operator[](size_t i) const; - virtual const Json::object &object_items() const; - virtual const Json &operator[](const std::string &key) const; - virtual ~JsonValue() {} -}; - -} // namespace lottiejson11 - -#endif \ No newline at end of file diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.cpp deleted file mode 100644 index 53f850f090b..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "CompositionLayer.hpp" - -namespace lottie { - -InvertedMatteLayer::InvertedMatteLayer(std::shared_ptr inputMatte) : -_inputMatte(inputMatte) { - setSize(inputMatte->size()); - - addSublayer(_inputMatte); -} - -std::shared_ptr makeInvertedMatteLayer(std::shared_ptr compositionLayer) { - auto result = std::make_shared(compositionLayer); - return result; -} - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp deleted file mode 100644 index e82b9c8b338..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp +++ /dev/null @@ -1,201 +0,0 @@ -#ifndef CompositionLayer_hpp -#define CompositionLayer_hpp - -#include -#include "Lottie/Public/Primitives/CALayer.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp" -#include "Lottie/Private/Model/Layers/LayerModel.hpp" -#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerTransformNode.hpp" -#include "Lottie/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.hpp" - -#include - -namespace lottie { - -class CompositionLayer; -class InvertedMatteLayer; - -/// A layer that inverses the alpha output of its input layer. -class InvertedMatteLayer: public CALayer { -public: - InvertedMatteLayer(std::shared_ptr inputMatte); - - std::shared_ptr _inputMatte; - - virtual bool isInvertedMatte() const override { - return true; - } -}; - -std::shared_ptr makeInvertedMatteLayer(std::shared_ptr compositionLayer); - -/// The base class for a child layer of CompositionContainer -class CompositionLayer: public CALayer, public KeypathSearchable { -public: - CompositionLayer(std::shared_ptr const &layer, Vector2D size) { - _contentsLayer = std::make_shared(); - - _transformNode = std::make_shared(layer->transform); - - if (layer->masks.has_value()) { - _maskLayer = std::make_shared(layer->masks.value()); - } else { - _maskLayer = nullptr; - } - - _matteType = layer->matte; - - _inFrame = layer->inFrame; - _outFrame = layer->outFrame; - _timeStretch = layer->timeStretch(); - _startFrame = layer->startTime; - if (layer->name.has_value()) { - _keypathName = layer->name.value(); - } else { - _keypathName = "Layer"; - } - - _childKeypaths.push_back(_transformNode->transformProperties()); - - _contentsLayer->setSize(size); - - if (layer->blendMode.has_value() && layer->blendMode.value() != BlendMode::Normal) { - setCompositingFilter(layer->blendMode); - } - - addSublayer(_contentsLayer); - - if (_maskLayer) { - _contentsLayer->setMask(_maskLayer); - } - } - - virtual std::string keypathName() const override { - return _keypathName; - } - - virtual std::map> keypathProperties() const override { - return {}; - } - - virtual std::shared_ptr keypathLayer() const override { - return _contentsLayer; - } - - void displayWithFrame(float frame, bool forceUpdates, BezierPathsBoundingBoxContext &boundingBoxContext) { - bool layerVisible = isInRangeOrEqual(frame, _inFrame, _outFrame); - - if (_transformNode->updateTree(frame, forceUpdates) || _contentsLayer->isHidden() != !layerVisible) { - _contentsLayer->setTransform(_transformNode->globalTransform()); - _contentsLayer->setOpacity(_transformNode->opacity()); - _contentsLayer->setIsHidden(!layerVisible); - - updateContentsLayerParameters(); - } - - /// Only update contents if current time is within the layers time bounds. - if (layerVisible) { - displayContentsWithFrame(frame, forceUpdates, boundingBoxContext); - if (_maskLayer) { - _maskLayer->updateWithFrame(frame, forceUpdates); - } - } - } - - virtual void updateContentsLayerParameters() { - } - - virtual void displayContentsWithFrame(float frame, bool forceUpdates, BezierPathsBoundingBoxContext &boundingBoxContext) { - /// To be overridden by subclass - } - - - virtual std::vector> const &childKeypaths() const override { - return _childKeypaths; - } - - std::shared_ptr _matteLayer; - void setMatteLayer(std::shared_ptr matteLayer) { - _matteLayer = matteLayer; - if (matteLayer) { - if (_matteType.has_value() && _matteType.value() == MatteType::Invert) { - setMask(makeInvertedMatteLayer(matteLayer)); - } else { - setMask(matteLayer); - } - } else { - setMask(nullptr); - } - } - - std::shared_ptr const &contentsLayer() const { - return _contentsLayer; - } - - std::shared_ptr const &maskLayer() const { - return _maskLayer; - } - void setMaskLayer(std::shared_ptr const &maskLayer) { - _maskLayer = maskLayer; - } - - std::optional const &matteType() const { - return _matteType; - } - - float inFrame() const { - return _inFrame; - } - float outFrame() const { - return _outFrame; - } - float startFrame() const { - return _startFrame; - } - float timeStretch() const { - return _timeStretch; - } - - virtual std::shared_ptr renderTreeNode(BezierPathsBoundingBoxContext &boundingBoxContext) { - return nullptr; - } - -public: - std::shared_ptr const transformNode() const { - return _transformNode; - } - -protected: - std::shared_ptr _contentsLayer; - std::optional _matteType; - -private: - std::shared_ptr _transformNode; - - std::shared_ptr _maskLayer; - - float _inFrame = 0.0; - float _outFrame = 0.0; - float _startFrame = 0.0; - float _timeStretch = 0.0; - - // MARK: Keypath Searchable - - std::string _keypathName; - -public: - virtual bool isImageCompositionLayer() const { - return false; - } - - virtual bool isTextCompositionLayer() const { - return false; - } - -protected: - std::vector> _childKeypaths; -}; - -} - -#endif /* CompositionLayer_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.cpp deleted file mode 100644 index ad3b669b24a..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "ImageCompositionLayer.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.hpp deleted file mode 100644 index ff6dc769fa0..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.hpp +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef ImageCompositionLayer_hpp -#define ImageCompositionLayer_hpp - -#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp" -#include "Lottie/Private/Model/Assets/ImageAsset.hpp" -#include "Lottie/Private/Model/Layers/ImageLayerModel.hpp" - -namespace lottie { - -class ImageCompositionLayer: public CompositionLayer { -public: - ImageCompositionLayer(std::shared_ptr const &imageLayer, Vector2D const &size) : - CompositionLayer(imageLayer, size) { - _imageReferenceID = imageLayer->referenceID; - - contentsLayer()->setMasksToBounds(true); - } - - std::shared_ptr image() { - return _image; - } - void setImage(std::shared_ptr image) { - _image = image; - //contentsLayer()->setContents(image); - } - - std::string const &imageReferenceID() { - return _imageReferenceID; - } - -public: - virtual bool isImageCompositionLayer() const override { - return true; - } - -private: - std::string _imageReferenceID; - std::shared_ptr _image; -}; - -} - -#endif /* ImageCompositionLayer_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.cpp deleted file mode 100644 index 398d52d57c1..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "MaskContainerLayer.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.hpp deleted file mode 100644 index 95bdb3f0932..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.hpp +++ /dev/null @@ -1,178 +0,0 @@ -#ifndef MaskContainerLayer_hpp -#define MaskContainerLayer_hpp - -#include "Lottie/Private/Model/Objects/Mask.hpp" -#include "Lottie/Public/Primitives/CALayer.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp" - -namespace lottie { - -inline MaskMode usableMaskMode(MaskMode mode) { - switch (mode) { - case MaskMode::Add: - return MaskMode::Add; - case MaskMode::Subtract: - return MaskMode::Subtract; - case MaskMode::Intersect: - return MaskMode::Intersect; - case MaskMode::Lighten: - return MaskMode::Add; - case MaskMode::Darken: - return MaskMode::Darken; - case MaskMode::Difference: - return MaskMode::Intersect; - case MaskMode::None: - return MaskMode::None; - } -} - -class MaskNodeProperties: public NodePropertyMap { -public: - MaskNodeProperties(std::shared_ptr const &mask) : - _mode(mask->mode()), - _inverted(mask->inverted) { - _opacity = std::make_shared>(std::make_shared>(mask->opacity->keyframes)); - _shape = std::make_shared>(std::make_shared>(mask->shape.keyframes)); - _expansion = std::make_shared>(std::make_shared>(mask->expansion->keyframes)); - - _propertyMap.insert(std::make_pair("Opacity", _opacity)); - _propertyMap.insert(std::make_pair("Shape", _shape)); - _propertyMap.insert(std::make_pair("Expansion", _expansion)); - - for (const auto &it : _propertyMap) { - _properties.push_back(it.second); - } - } - - virtual std::vector> &properties() override { - return _properties; - } - - virtual std::vector> const &childKeypaths() const override { - return _childKeypaths; - } - - std::shared_ptr> const &opacity() const { - return _opacity; - } - - std::shared_ptr> const &shape() const { - return _shape; - } - - std::shared_ptr> const &expansion() const { - return _expansion; - } - - MaskMode mode() const { - return _mode; - } - - bool inverted() const { - return _inverted; - } - -private: - std::map> _propertyMap; - std::vector> _childKeypaths; - - std::vector> _properties; - - MaskMode _mode = MaskMode::Add; - bool _inverted = false; - - std::shared_ptr> _opacity; - std::shared_ptr> _shape; - std::shared_ptr> _expansion; -}; - -class MaskLayer: public CALayer { -public: - MaskLayer(std::shared_ptr const &mask) : - _properties(mask) { - _maskLayer = std::make_shared(); - - addSublayer(_maskLayer); - - if (mask->mode() == MaskMode::Add) { - _maskLayer->setFillColor(Color(1.0, 0.0, 0.0, 1.0)); - } else { - _maskLayer->setFillColor(Color(0.0, 1.0, 0.0, 1.0)); - } - _maskLayer->setFillRule(FillRule::EvenOdd); - } - - virtual ~MaskLayer() = default; - - void updateWithFrame(float frame, bool forceUpdates) { - if (_properties.opacity()->needsUpdate(frame) || forceUpdates) { - _properties.opacity()->update(frame); - setOpacity(_properties.opacity()->value().value); - } - - if (_properties.shape()->needsUpdate(frame) || forceUpdates) { - _properties.shape()->update(frame); - _properties.expansion()->update(frame); - - auto path = _properties.shape()->value().cgPath(); - auto usableMode = usableMaskMode(_properties.mode()); - if ((usableMode == MaskMode::Subtract && !_properties.inverted()) || - (usableMode == MaskMode::Add && _properties.inverted())) { - /// Add a bounds rect to invert the mask - auto newPath = CGPath::makePath(); - newPath->addRect(CGRect::veryLarge()); - newPath->addPath(path); - path = std::static_pointer_cast(newPath); - } - _maskLayer->setPath(path); - } - } - -private: - MaskNodeProperties _properties; - - std::shared_ptr _maskLayer; -}; - -class MaskContainerLayer: public CALayer { -public: - MaskContainerLayer(std::vector> const &masks) { - auto containerLayer = std::make_shared(); - bool firstObject = true; - for (const auto &mask : masks) { - auto maskLayer = std::make_shared(mask); - _maskLayers.push_back(maskLayer); - - auto usableMode = usableMaskMode(mask->mode()); - if (usableMode == MaskMode::None) { - continue; - } else if (usableMode == MaskMode::Add || firstObject) { - firstObject = false; - containerLayer->addSublayer(maskLayer); - } else { - containerLayer->setMask(maskLayer); - auto newContainer = std::make_shared(); - newContainer->addSublayer(containerLayer); - containerLayer = newContainer; - } - } - addSublayer(containerLayer); - } - - // MARK: Internal - - void updateWithFrame(float frame, bool forceUpdates) { - for (const auto &maskLayer : _maskLayers) { - maskLayer->updateWithFrame(frame, forceUpdates); - } - } - -private: - std::vector> _maskLayers; -}; - -} - -#endif /* MaskContainerLayer_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.cpp deleted file mode 100644 index 6c5345cfd61..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "NullCompositionLayer.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.hpp deleted file mode 100644 index c3d3dea5cc0..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef NullCompositionLayer_hpp -#define NullCompositionLayer_hpp - -#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp" - -namespace lottie { - -class NullCompositionLayer: public CompositionLayer { -public: - NullCompositionLayer(std::shared_ptr const &layer) : - CompositionLayer(layer, Vector2D::Zero()) { - } -}; - -} - -#endif /* NullCompositionLayer_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.cpp deleted file mode 100644 index d3428a9b1b0..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "PreCompositionLayer.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.hpp deleted file mode 100644 index c170f21a582..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.hpp +++ /dev/null @@ -1,202 +0,0 @@ -#ifndef PreCompositionLayer_hpp -#define PreCompositionLayer_hpp - -#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp" -#include "Lottie/Private/Model/Layers/PreCompLayerModel.hpp" -#include "Lottie/Private/Model/Assets/PrecompAsset.hpp" -#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerImageProvider.hpp" -#include "Lottie/Public/TextProvider/AnimationTextProvider.hpp" -#include "Lottie/Public/FontProvider/AnimationFontProvider.hpp" -#include "Lottie/Private/Model/Assets/AssetLibrary.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp" -#include "Lottie/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.hpp" - -namespace lottie { - -class PreCompositionLayer: public CompositionLayer { -public: - PreCompositionLayer( - std::shared_ptr const &precomp, - PrecompAsset const &asset, - std::shared_ptr const &layerImageProvider, - std::shared_ptr const &textProvider, - std::shared_ptr const &fontProvider, - std::shared_ptr const &assetLibrary, - float frameRate - ) : CompositionLayer(precomp, Vector2D(precomp->width, precomp->height)) { - if (precomp->timeRemapping) { - _remappingNode = std::make_shared>(std::make_shared>(precomp->timeRemapping->keyframes)); - } - _frameRate = frameRate; - - setSize(Vector2D(precomp->width, precomp->height)); - contentsLayer()->setMasksToBounds(true); - contentsLayer()->setSize(size()); - - auto layers = initializeCompositionLayers( - asset.layers, - assetLibrary, - layerImageProvider, - textProvider, - fontProvider, - frameRate - ); - - std::vector> imageLayers; - - std::shared_ptr mattedLayer; - - for (auto layerIt = layers.rbegin(); layerIt != layers.rend(); layerIt++) { - std::shared_ptr layer = *layerIt; - layer->setSize(size()); - _animationLayers.push_back(layer); - - if (layer->isImageCompositionLayer()) { - imageLayers.push_back(std::static_pointer_cast(layer)); - } - if (mattedLayer) { - /// The previous layer requires this layer to be its matte - mattedLayer->setMatteLayer(layer); - mattedLayer = nullptr; - continue; - } - if (layer->matteType().has_value() && (layer->matteType().value() == MatteType::Add || layer->matteType().value() == MatteType::Invert)) { - /// We have a layer that requires a matte. - mattedLayer = layer; - } - contentsLayer()->addSublayer(layer); - } - - for (const auto &layer : layers) { - _childKeypaths.push_back(layer); - } - - layerImageProvider->addImageLayers(imageLayers); - } - - virtual std::map> keypathProperties() const override { - if (!_remappingNode) { - return {}; - } - - std::map> result; - result.insert(std::make_pair("Time Remap", _remappingNode)); - - return result; - } - - virtual void displayContentsWithFrame(float frame, bool forceUpdates, BezierPathsBoundingBoxContext &boundingBoxContext) override { - float localFrame = 0.0; - if (_remappingNode) { - _remappingNode->update(frame); - localFrame = _remappingNode->value().value * _frameRate; - } else { - localFrame = (frame - startFrame()) / timeStretch(); - } - - for (const auto &animationLayer : _animationLayers) { - animationLayer->displayWithFrame(localFrame, forceUpdates, boundingBoxContext); - } - } - - virtual std::shared_ptr renderTreeNode(BezierPathsBoundingBoxContext &boundingBoxContext) override { - if (!_renderTreeNode) { - std::vector> renderTreeSubnodes; - for (const auto &animationLayer : _animationLayers) { - bool found = false; - for (const auto &sublayer : contentsLayer()->sublayers()) { - if (animationLayer == sublayer) { - found = true; - break; - } - } - if (found) { - auto node = animationLayer->renderTreeNode(boundingBoxContext); - if (node) { - renderTreeSubnodes.push_back(node); - } - } - } - - std::vector> renderTreeValue; - auto renderTreeContentItem = std::make_shared( - Vector2D(0.0, 0.0), - Transform2D::identity(), - 1.0, - false, - false, - renderTreeSubnodes, - nullptr, - false - ); - if (renderTreeContentItem) { - renderTreeValue.push_back(renderTreeContentItem); - } - - _contentsTreeNode = std::make_shared( - Vector2D(0.0, 0.0), - Transform2D::identity(), - 1.0, - false, - false, - renderTreeValue, - nullptr, - false - ); - - std::vector> subnodes; - subnodes.push_back(_contentsTreeNode); - - std::shared_ptr maskNode; - bool invertMask = false; - if (_matteLayer) { - maskNode = _matteLayer->renderTreeNode(boundingBoxContext); - if (maskNode && _matteType.has_value() && _matteType.value() == MatteType::Invert) { - invertMask = true; - } - } - - _renderTreeNode = std::make_shared( - Vector2D(0.0, 0.0), - Transform2D::identity(), - 1.0, - false, - false, - subnodes, - maskNode, - invertMask - ); - } - - _contentsTreeNode->_size = _contentsLayer->size(); - _contentsTreeNode->_masksToBounds = _contentsLayer->masksToBounds(); - - _renderTreeNode->_size = size(); - _renderTreeNode->_transform = transform(); - _renderTreeNode->_alpha = opacity(); - _renderTreeNode->_masksToBounds = masksToBounds(); - _renderTreeNode->_isHidden = isHidden(); - - return _renderTreeNode; - } - - virtual void updateContentsLayerParameters() override { - _contentsTreeNode->_transform = _contentsLayer->transform(); - _contentsTreeNode->_alpha = _contentsLayer->opacity(); - _contentsTreeNode->_isHidden = _contentsLayer->isHidden(); - } - -private: - float _frameRate = 0.0; - std::shared_ptr> _remappingNode; - - std::vector> _animationLayers; - - std::shared_ptr _renderTreeNode; - std::shared_ptr _contentsTreeNode; -}; - -} - -#endif /* PreCompositionLayer_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp deleted file mode 100644 index e7ad955e0f2..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp +++ /dev/null @@ -1,1378 +0,0 @@ -#include "ShapeCompositionLayer.hpp" - -#include "Lottie/Private/Model/ShapeItems/Group.hpp" -#include "Lottie/Private/Model/ShapeItems/Ellipse.hpp" -#include "Lottie/Private/Model/ShapeItems/Rectangle.hpp" -#include "Lottie/Private/Model/ShapeItems/Star.hpp" -#include "Lottie/Private/Model/ShapeItems/Shape.hpp" -#include "Lottie/Private/Model/ShapeItems/Trim.hpp" -#include "Lottie/Private/Model/ShapeItems/Stroke.hpp" -#include "Lottie/Private/Model/ShapeItems/GradientStroke.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/RenderLayers/GetGradientParameters.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.hpp" -#include "Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeUtils/BezierPathUtils.hpp" -#include "Lottie/Private/Model/ShapeItems/ShapeTransform.hpp" - -namespace lottie { - -class ShapeLayerPresentationTree { -public: - class FillOutput { - public: - FillOutput() { - } - ~FillOutput() = default; - - virtual void update(AnimationFrameTime frameTime) = 0; - virtual std::shared_ptr fill() = 0; - }; - - class SolidFillOutput : public FillOutput { - public: - explicit SolidFillOutput(Fill const &fill) : - rule(fill.fillRule.value_or(FillRule::NonZeroWinding)), - color(fill.color.keyframes), - opacity(fill.opacity.keyframes) { - auto solid = std::make_shared(Color(0.0, 0.0, 0.0, 0.0), 0.0); - _fill = std::make_shared( - solid, - rule - ); - } - - virtual ~SolidFillOutput() = default; - - virtual void update(AnimationFrameTime frameTime) override { - bool hasUpdates = false; - - if (color.hasUpdate(frameTime)) { - hasUpdates = true; - colorValue = color.value(frameTime); - } - - if (opacity.hasUpdate(frameTime)) { - hasUpdates = true; - opacityValue = opacity.value(frameTime).value; - } - - if (hasUpdates) { - RenderTreeNodeContentItem::SolidShading *solid = (RenderTreeNodeContentItem::SolidShading *)_fill->shading.get(); - solid->color = colorValue; - solid->opacity = opacityValue * 0.01; - } - } - - virtual std::shared_ptr fill() override { - return _fill; - } - - private: - FillRule rule; - - KeyframeInterpolator color; - Color colorValue = Color(0.0, 0.0, 0.0, 0.0); - - KeyframeInterpolator opacity; - float opacityValue = 0.0; - - std::shared_ptr _fill; - }; - - class GradientFillOutput : public FillOutput { - public: - explicit GradientFillOutput(GradientFill const &gradientFill) : - rule(FillRule::NonZeroWinding), - numberOfColors(gradientFill.numberOfColors), - gradientType(gradientFill.gradientType), - colors(gradientFill.colors.keyframes), - startPoint(gradientFill.startPoint.keyframes), - endPoint(gradientFill.endPoint.keyframes), - opacity(gradientFill.opacity.keyframes) { - auto gradient = std::make_shared( - 0.0, - gradientType, - std::vector(), - std::vector(), - Vector2D(0.0, 0.0), - Vector2D(0.0, 0.0) - ); - _fill = std::make_shared( - gradient, - rule - ); - } - - virtual ~GradientFillOutput() = default; - - virtual void update(AnimationFrameTime frameTime) override { - bool hasUpdates = false; - - if (colors.hasUpdate(frameTime)) { - hasUpdates = true; - colorsValue = colors.value(frameTime); - } - - if (startPoint.hasUpdate(frameTime)) { - hasUpdates = true; - startPointValue = startPoint.value(frameTime); - } - - if (endPoint.hasUpdate(frameTime)) { - hasUpdates = true; - endPointValue = endPoint.value(frameTime); - } - - if (opacity.hasUpdate(frameTime)) { - hasUpdates = true; - opacityValue = opacity.value(frameTime).value; - } - - if (hasUpdates) { - std::vector colors; - std::vector locations; - getGradientParameters(numberOfColors, colorsValue, colors, locations); - - RenderTreeNodeContentItem::GradientShading *gradient = ((RenderTreeNodeContentItem::GradientShading *)_fill->shading.get()); - gradient->opacity = opacityValue * 0.01; - gradient->colors = colors; - gradient->locations = locations; - gradient->start = Vector2D(startPointValue.x, startPointValue.y); - gradient->end = Vector2D(endPointValue.x, endPointValue.y); - } - } - - virtual std::shared_ptr fill() override { - return _fill; - } - - private: - FillRule rule; - int numberOfColors = 0; - GradientType gradientType; - - KeyframeInterpolator colors; - GradientColorSet colorsValue; - - KeyframeInterpolator startPoint; - Vector3D startPointValue = Vector3D(0.0, 0.0, 0.0); - - KeyframeInterpolator endPoint; - Vector3D endPointValue = Vector3D(0.0, 0.0, 0.0); - - KeyframeInterpolator opacity; - float opacityValue = 0.0; - - std::shared_ptr _fill; - }; - - class StrokeOutput { - public: - StrokeOutput() { - } - ~StrokeOutput() = default; - - virtual void update(AnimationFrameTime frameTime) = 0; - virtual std::shared_ptr stroke() = 0; - }; - - class SolidStrokeOutput : public StrokeOutput { - public: - SolidStrokeOutput(Stroke const &stroke) : - lineJoin(stroke.lineJoin), - lineCap(stroke.lineCap), - miterLimit(stroke.miterLimit.value_or(4.0)), - color(stroke.color.keyframes), - opacity(stroke.opacity.keyframes), - width(stroke.width.keyframes) { - if (stroke.dashPattern.has_value()) { - StrokeShapeDashConfiguration dashConfiguration(stroke.dashPattern.value()); - dashPattern = std::make_unique(dashConfiguration.dashPatterns); - - if (!dashConfiguration.dashPhase.empty()) { - dashPhase = std::make_unique>(dashConfiguration.dashPhase); - } - } - - auto solid = std::make_shared(Color(0.0, 0.0, 0.0, 0.0), 0.0); - _stroke = std::make_shared( - solid, - 0.0, - lineJoin, - lineCap, - miterLimit, - 0.0, - std::vector() - ); - } - - virtual ~SolidStrokeOutput() = default; - - virtual void update(AnimationFrameTime frameTime) override { - bool hasUpdates = false; - - if (color.hasUpdate(frameTime)) { - hasUpdates = true; - colorValue = color.value(frameTime); - } - - if (opacity.hasUpdate(frameTime)) { - hasUpdates = true; - opacityValue = opacity.value(frameTime).value; - } - - if (width.hasUpdate(frameTime)) { - hasUpdates = true; - widthValue = width.value(frameTime).value; - } - - if (dashPattern) { - if (dashPattern->hasUpdate(frameTime)) { - hasUpdates = true; - dashPatternValue = dashPattern->value(frameTime); - } - } - - if (dashPhase) { - if (dashPhase->hasUpdate(frameTime)) { - hasUpdates = true; - dashPhaseValue = dashPhase->value(frameTime).value; - } - } - - if (hasUpdates) { - bool hasNonZeroDashes = false; - if (!dashPatternValue.values.empty()) { - for (const auto &value : dashPatternValue.values) { - if (value != 0) { - hasNonZeroDashes = true; - break; - } - } - } - - RenderTreeNodeContentItem::SolidShading *solid = (RenderTreeNodeContentItem::SolidShading *)_stroke->shading.get(); - solid->color = colorValue; - solid->opacity = opacityValue * 0.01; - - _stroke->lineWidth = widthValue; - _stroke->dashPhase = hasNonZeroDashes ? dashPhaseValue : 0.0; - _stroke->dashPattern = hasNonZeroDashes ? dashPatternValue.values : std::vector(); - } - } - - virtual std::shared_ptr stroke() override { - return _stroke; - } - - private: - LineJoin lineJoin; - LineCap lineCap; - float miterLimit = 4.0; - - KeyframeInterpolator color; - Color colorValue = Color(0.0, 0.0, 0.0, 0.0); - - KeyframeInterpolator opacity; - float opacityValue = 0.0; - - KeyframeInterpolator width; - float widthValue = 0.0; - - std::unique_ptr dashPattern; - DashPattern dashPatternValue = DashPattern({}); - - std::unique_ptr> dashPhase; - float dashPhaseValue = 0.0; - - std::shared_ptr _stroke; - }; - - class GradientStrokeOutput : public StrokeOutput { - public: - GradientStrokeOutput(GradientStroke const &gradientStroke) : - lineJoin(gradientStroke.lineJoin), - lineCap(gradientStroke.lineCap), - miterLimit(gradientStroke.miterLimit.value_or(4.0)), - numberOfColors(gradientStroke.numberOfColors), - gradientType(gradientStroke.gradientType), - colors(gradientStroke.colors.keyframes), - startPoint(gradientStroke.startPoint.keyframes), - endPoint(gradientStroke.endPoint.keyframes), - opacity(gradientStroke.opacity.keyframes), - width(gradientStroke.width.keyframes) { - if (gradientStroke.dashPattern.has_value()) { - StrokeShapeDashConfiguration dashConfiguration(gradientStroke.dashPattern.value()); - dashPattern = std::make_unique(dashConfiguration.dashPatterns); - - if (!dashConfiguration.dashPhase.empty()) { - dashPhase = std::make_unique>(dashConfiguration.dashPhase); - } - } - - auto gradient = std::make_shared( - 0.0, - gradientType, - std::vector(), - std::vector(), - Vector2D(0.0, 0.0), - Vector2D(0.0, 0.0) - ); - _stroke = std::make_shared( - gradient, - 0.0, - lineJoin, - lineCap, - miterLimit, - 0.0, - std::vector() - ); - } - - virtual ~GradientStrokeOutput() = default; - - virtual void update(AnimationFrameTime frameTime) override { - bool hasUpdates = false; - - if (colors.hasUpdate(frameTime)) { - hasUpdates = true; - colorsValue = colors.value(frameTime); - } - - if (startPoint.hasUpdate(frameTime)) { - hasUpdates = true; - startPointValue = startPoint.value(frameTime); - } - - if (endPoint.hasUpdate(frameTime)) { - hasUpdates = true; - endPointValue = endPoint.value(frameTime); - } - - if (opacity.hasUpdate(frameTime)) { - hasUpdates = true; - opacityValue = opacity.value(frameTime).value; - } - - if (width.hasUpdate(frameTime)) { - hasUpdates = true; - widthValue = width.value(frameTime).value; - } - - if (dashPattern) { - if (dashPattern->hasUpdate(frameTime)) { - hasUpdates = true; - dashPatternValue = dashPattern->value(frameTime); - } - } - - if (dashPhase) { - if (dashPhase->hasUpdate(frameTime)) { - hasUpdates = true; - dashPhaseValue = dashPhase->value(frameTime).value; - } - } - - if (hasUpdates) { - bool hasNonZeroDashes = false; - if (!dashPatternValue.values.empty()) { - for (const auto &value : dashPatternValue.values) { - if (value != 0) { - hasNonZeroDashes = true; - break; - } - } - } - - std::vector colors; - std::vector locations; - getGradientParameters(numberOfColors, colorsValue, colors, locations); - - RenderTreeNodeContentItem::GradientShading *gradient = ((RenderTreeNodeContentItem::GradientShading *)_stroke->shading.get()); - gradient->opacity = opacityValue * 0.01; - gradient->colors = colors; - gradient->locations = locations; - gradient->start = Vector2D(startPointValue.x, startPointValue.y); - gradient->end = Vector2D(endPointValue.x, endPointValue.y); - - _stroke->lineWidth = widthValue; - _stroke->dashPhase = hasNonZeroDashes ? dashPhaseValue : 0.0; - _stroke->dashPattern = hasNonZeroDashes ? dashPatternValue.values : std::vector(); - } - } - - virtual std::shared_ptr stroke() override { - return _stroke; - } - - private: - LineJoin lineJoin; - LineCap lineCap; - float miterLimit = 4.0; - - int numberOfColors = 0; - GradientType gradientType; - - KeyframeInterpolator colors; - GradientColorSet colorsValue; - - KeyframeInterpolator startPoint; - Vector3D startPointValue = Vector3D(0.0, 0.0, 0.0); - - KeyframeInterpolator endPoint; - Vector3D endPointValue = Vector3D(0.0, 0.0, 0.0); - - KeyframeInterpolator opacity; - float opacityValue = 0.0; - - KeyframeInterpolator width; - float widthValue = 0.0; - - std::unique_ptr dashPattern; - DashPattern dashPatternValue = DashPattern({}); - - std::unique_ptr> dashPhase; - float dashPhaseValue = 0.0; - - std::shared_ptr _stroke; - }; - - class TrimParamsOutput { - public: - TrimParamsOutput(Trim const &trim) : - type(trim.trimType), - start(trim.start.keyframes), - end(trim.end.keyframes), - offset(trim.offset.keyframes) { - } - - void update(AnimationFrameTime frameTime) { - if (start.hasUpdate(frameTime)) { - startValue = start.value(frameTime).value; - } - - if (end.hasUpdate(frameTime)) { - endValue = end.value(frameTime).value; - } - - if (offset.hasUpdate(frameTime)) { - offsetValue = offset.value(frameTime).value; - } - } - - TrimParams trimParams() { - float resolvedStartValue = startValue * 0.01; - float resolvedEndValue = endValue * 0.01; - float resolvedStart = std::min(resolvedStartValue, resolvedEndValue); - float resolvedEnd = std::max(resolvedStartValue, resolvedEndValue); - - float resolvedOffset = fmod(offsetValue, 360.0) / 360.0; - - return TrimParams(resolvedStart, resolvedEnd, resolvedOffset, type); - } - - private: - TrimType type; - - KeyframeInterpolator start; - float startValue = 0.0; - - KeyframeInterpolator end; - float endValue = 0.0; - - KeyframeInterpolator offset; - float offsetValue = 0.0; - }; - - struct ShadingVariant { - std::shared_ptr fill; - std::shared_ptr stroke; - size_t subItemLimit = 0; - }; - - struct TransformedPath { - BezierPath path; - Transform2D transform; - - TransformedPath(BezierPath const &path_, Transform2D const &transform_) : - path(path_), - transform(transform_) { - } - }; - - class PathOutput { - public: - PathOutput() { - } - virtual ~PathOutput() = default; - - virtual void update(AnimationFrameTime frameTime) = 0; - virtual std::shared_ptr ¤tPath() = 0; - }; - - class StaticPathOutput : public PathOutput { - public: - explicit StaticPathOutput(BezierPath const &path) : - resolvedPath(std::make_shared(path)) { - } - - virtual void update(AnimationFrameTime frameTime) override { - } - - virtual std::shared_ptr ¤tPath() override { - return resolvedPath; - } - - private: - std::shared_ptr resolvedPath; - }; - - class ShapePathOutput : public PathOutput { - public: - explicit ShapePathOutput(Shape const &shape) : - path(shape.path.keyframes), - resolvedPath(std::make_shared(BezierPath())) { - } - - virtual void update(AnimationFrameTime frameTime) override { - if (!hasValidData || path.hasUpdate(frameTime)) { - path.update(frameTime, resolvedPath->path); - resolvedPath->needsBoundsRecalculation = true; - } - hasValidData = true; - } - - virtual std::shared_ptr ¤tPath() override { - return resolvedPath; - } - - private: - bool hasValidData = false; - - BezierPathKeyframeInterpolator path; - - std::shared_ptr resolvedPath; - }; - - class RectanglePathOutput : public PathOutput { - public: - explicit RectanglePathOutput(Rectangle const &rectangle) : - direction(rectangle.direction.value_or(PathDirection::Clockwise)), - position(rectangle.position.keyframes), - size(rectangle.size.keyframes), - cornerRadius(rectangle.cornerRadius.keyframes), - resolvedPath(std::make_shared(BezierPath())) { - } - - virtual void update(AnimationFrameTime frameTime) override { - bool hasUpdates = false; - - if (!hasValidData || position.hasUpdate(frameTime)) { - hasUpdates = true; - positionValue = position.value(frameTime); - } - if (!hasValidData || size.hasUpdate(frameTime)) { - hasUpdates = true; - sizeValue = size.value(frameTime); - } - if (!hasValidData || cornerRadius.hasUpdate(frameTime)) { - hasUpdates = true; - cornerRadiusValue = cornerRadius.value(frameTime).value; - } - - if (hasUpdates) { - ValueInterpolator::setInplace(makeRectangleBezierPath(Vector2D(positionValue.x, positionValue.y), Vector2D(sizeValue.x, sizeValue.y), cornerRadiusValue, direction), resolvedPath->path); - resolvedPath->needsBoundsRecalculation = true; - } - - hasValidData = true; - } - - virtual std::shared_ptr ¤tPath() override { - return resolvedPath; - } - - private: - bool hasValidData = false; - - PathDirection direction; - - KeyframeInterpolator position; - Vector3D positionValue = Vector3D(0.0, 0.0, 0.0); - - KeyframeInterpolator size; - Vector3D sizeValue = Vector3D(0.0, 0.0, 0.0); - - KeyframeInterpolator cornerRadius; - float cornerRadiusValue = 0.0; - - std::shared_ptr resolvedPath; - }; - - class EllipsePathOutput : public PathOutput { - public: - explicit EllipsePathOutput(Ellipse const &ellipse) : - direction(ellipse.direction.value_or(PathDirection::Clockwise)), - position(ellipse.position.keyframes), - size(ellipse.size.keyframes), - resolvedPath(std::make_shared(BezierPath())) { - } - - virtual void update(AnimationFrameTime frameTime) override { - bool hasUpdates = false; - - if (!hasValidData || position.hasUpdate(frameTime)) { - hasUpdates = true; - positionValue = position.value(frameTime); - } - if (!hasValidData || size.hasUpdate(frameTime)) { - hasUpdates = true; - sizeValue = size.value(frameTime); - } - - if (hasUpdates) { - ValueInterpolator::setInplace(makeEllipseBezierPath(Vector2D(sizeValue.x, sizeValue.y), Vector2D(positionValue.x, positionValue.y), direction), resolvedPath->path); - resolvedPath->needsBoundsRecalculation = true; - } - - hasValidData = true; - } - - virtual std::shared_ptr ¤tPath() override { - return resolvedPath; - } - - private: - bool hasValidData = false; - - PathDirection direction; - - KeyframeInterpolator position; - Vector3D positionValue = Vector3D(0.0, 0.0, 0.0); - - KeyframeInterpolator size; - Vector3D sizeValue = Vector3D(0.0, 0.0, 0.0); - - std::shared_ptr resolvedPath; - }; - - class StarPathOutput : public PathOutput { - public: - explicit StarPathOutput(Star const &star) : - direction(star.direction.value_or(PathDirection::Clockwise)), - position(star.position.keyframes), - outerRadius(star.outerRadius.keyframes), - outerRoundedness(star.outerRoundness.keyframes), - rotation(star.rotation.keyframes), - points(star.points.keyframes), - resolvedPath(std::make_shared(BezierPath())) { - if (star.innerRadius.has_value()) { - innerRadius = std::make_unique>(std::make_shared>(star.innerRadius->keyframes)); - } else { - innerRadius = std::make_unique>(std::make_shared>(Vector1D(0.0))); - } - - if (star.innerRoundness.has_value()) { - innerRoundedness = std::make_unique>(std::make_shared>(star.innerRoundness->keyframes)); - } else { - innerRoundedness = std::make_unique>(std::make_shared>(Vector1D(0.0))); - } - } - - virtual void update(AnimationFrameTime frameTime) override { - bool hasUpdates = false; - - if (!hasValidData || position.hasUpdate(frameTime)) { - hasUpdates = true; - positionValue = position.value(frameTime); - } - - if (!hasValidData || outerRadius.hasUpdate(frameTime)) { - hasUpdates = true; - outerRadiusValue = outerRadius.value(frameTime).value; - } - - innerRadius->update(frameTime); - if (!hasValidData || innerRadiusValue != innerRadius->value().value) { - hasUpdates = true; - innerRadiusValue = innerRadius->value().value; - } - - if (!hasValidData || outerRoundedness.hasUpdate(frameTime)) { - hasUpdates = true; - outerRoundednessValue = outerRoundedness.value(frameTime).value; - } - - innerRoundedness->update(frameTime); - if (!hasValidData || innerRoundednessValue != innerRoundedness->value().value) { - hasUpdates = true; - innerRoundednessValue = innerRoundedness->value().value; - } - - if (!hasValidData || points.hasUpdate(frameTime)) { - hasUpdates = true; - pointsValue = points.value(frameTime).value; - } - - if (!hasValidData || rotation.hasUpdate(frameTime)) { - hasUpdates = true; - rotationValue = rotation.value(frameTime).value; - } - - if (hasUpdates) { - ValueInterpolator::setInplace(makeStarBezierPath(Vector2D(positionValue.x, positionValue.y), outerRadiusValue, innerRadiusValue, outerRoundednessValue, innerRoundednessValue, pointsValue, rotationValue, direction), resolvedPath->path); - resolvedPath->needsBoundsRecalculation = true; - } - - hasValidData = true; - } - - virtual std::shared_ptr ¤tPath() override { - return resolvedPath; - } - - private: - bool hasValidData = false; - - PathDirection direction; - - KeyframeInterpolator position; - Vector3D positionValue = Vector3D(0.0, 0.0, 0.0); - - KeyframeInterpolator outerRadius; - float outerRadiusValue = 0.0; - - KeyframeInterpolator outerRoundedness; - float outerRoundednessValue = 0.0; - - std::unique_ptr> innerRadius; - float innerRadiusValue = 0.0; - - std::unique_ptr> innerRoundedness; - float innerRoundednessValue = 0.0; - - KeyframeInterpolator rotation; - float rotationValue = 0.0; - - KeyframeInterpolator points; - float pointsValue = 0.0; - - std::shared_ptr resolvedPath; - }; - - class TransformOutput { - public: - TransformOutput(std::shared_ptr shapeTransform) { - if (shapeTransform->anchor) { - _anchor = std::make_unique>(shapeTransform->anchor->keyframes); - } - if (shapeTransform->position) { - _position = std::make_unique>(shapeTransform->position->keyframes); - } - if (shapeTransform->scale) { - _scale = std::make_unique>(shapeTransform->scale->keyframes); - } - if (shapeTransform->rotation) { - _rotation = std::make_unique>(shapeTransform->rotation->keyframes); - } - if (shapeTransform->skew) { - _skew = std::make_unique>(shapeTransform->skew->keyframes); - } - if (shapeTransform->skewAxis) { - _skewAxis = std::make_unique>(shapeTransform->skewAxis->keyframes); - } - if (shapeTransform->opacity) { - _opacity = std::make_unique>(shapeTransform->opacity->keyframes); - } - } - - void update(AnimationFrameTime frameTime) { - bool hasUpdates = false; - - if (!hasValidData) { - hasUpdates = true; - } - if (_anchor && _anchor->hasUpdate(frameTime)) { - hasUpdates = true; - } - if (_position && _position->hasUpdate(frameTime)) { - hasUpdates = true; - } - if (_scale && _scale->hasUpdate(frameTime)) { - hasUpdates = true; - } - if (_rotation && _rotation->hasUpdate(frameTime)) { - hasUpdates = true; - } - if (_skew && _skew->hasUpdate(frameTime)) { - hasUpdates = true; - } - if (_skewAxis && _skewAxis->hasUpdate(frameTime)) { - hasUpdates = true; - } - if (_opacity && _opacity->hasUpdate(frameTime)) { - hasUpdates = true; - } - - if (hasUpdates) { - //TODO:optimize by storing components - - Vector3D anchorValue(0.0, 0.0, 0.0); - if (_anchor) { - anchorValue = _anchor->value(frameTime); - } - - Vector3D positionValue(0.0, 0.0, 0.0); - if (_position) { - positionValue = _position->value(frameTime); - } - - Vector3D scaleValue(100.0, 100.0, 100.0); - if (_scale) { - scaleValue = _scale->value(frameTime); - } - - float rotationValue = 0.0; - if (_rotation) { - rotationValue = _rotation->value(frameTime).value; - } - - float skewValue = 0.0; - if (_skew) { - skewValue = _skew->value(frameTime).value; - } - - float skewAxisValue = 0.0; - if (_skewAxis) { - skewAxisValue = _skewAxis->value(frameTime).value; - } - - if (_opacity) { - _opacityValue = _opacity->value(frameTime).value * 0.01; - } else { - _opacityValue = 1.0; - } - - _transformValue = Transform2D::identity().translated(Vector2D(positionValue.x, positionValue.y)).rotated(rotationValue).skewed(-skewValue, skewAxisValue).scaled(Vector2D(scaleValue.x * 0.01, scaleValue.y * 0.01)).translated(Vector2D(-anchorValue.x, -anchorValue.y)); - - hasValidData = true; - } - } - - Transform2D const &transform() { - return _transformValue; - } - - float opacity() { - return _opacityValue; - } - - private: - bool hasValidData = false; - - std::unique_ptr> _anchor; - std::unique_ptr> _position; - std::unique_ptr> _scale; - std::unique_ptr> _rotation; - std::unique_ptr> _skew; - std::unique_ptr> _skewAxis; - std::unique_ptr> _opacity; - - Transform2D _transformValue = Transform2D::identity(); - float _opacityValue = 1.0; - }; - - class ContentItem { - public: - ContentItem() { - } - - public: - bool isGroup = false; - - void setPath(std::unique_ptr &&path_) { - path = std::move(path_); - } - - void setTransform(std::unique_ptr &&transform_) { - transform = std::move(transform_); - } - - private: - std::unique_ptr path; - std::unique_ptr transform; - - std::vector shadings; - std::vector> trims; - - public: - std::vector> subItems; - std::shared_ptr _contentItem; - - private: - std::vector collectPaths(size_t subItemLimit, Transform2D const &parentTransform, bool skipApplyTransform) { - std::vector mappedPaths; - - //TODO:remove skipApplyTransform - Transform2D effectiveTransform = parentTransform; - if (!skipApplyTransform && isGroup && transform) { - effectiveTransform = transform->transform() * effectiveTransform; - } - - size_t maxSubitem = std::min(subItems.size(), subItemLimit); - - if (_contentItem->path) { - mappedPaths.emplace_back(_contentItem->path->path, effectiveTransform); - } - - for (size_t i = 0; i < maxSubitem; i++) { - auto &subItem = subItems[i]; - - std::optional currentTrim; - if (!trims.empty()) { - currentTrim = trims[0]->trimParams(); - } - - auto subItemPaths = subItem->collectPaths(INT32_MAX, effectiveTransform, false); - - if (currentTrim) { - CompoundBezierPath tempPath; - for (auto &path : subItemPaths) { - tempPath.appendPath(path.path.copyUsingTransform(path.transform)); - } - CompoundBezierPath trimmedPath = trimCompoundPath(tempPath, currentTrim->start, currentTrim->end, currentTrim->offset, currentTrim->type); - for (auto &path : trimmedPath.paths) { - mappedPaths.emplace_back(path, Transform2D::identity()); - } - } else { - for (auto &path : subItemPaths) { - mappedPaths.emplace_back(path.path, path.transform); - } - } - } - - return mappedPaths; - } - - public: - void addSubItem(std::shared_ptr const &subItem) { - subItems.push_back(subItem); - } - - void addFill(std::shared_ptr fill) { - ShadingVariant shading; - shading.subItemLimit = subItems.size(); - shading.fill = fill; - shadings.insert(shadings.begin(), shading); - } - - void addStroke(std::shared_ptr stroke) { - ShadingVariant shading; - shading.subItemLimit = subItems.size(); - shading.stroke = stroke; - shadings.insert(shadings.begin(), shading); - } - - void addTrim(Trim const &trim) { - trims.push_back(std::make_shared(trim)); - } - - public: - void initializeRenderChildren() { - _contentItem = std::make_shared(); - _contentItem->isGroup = isGroup; - - if (path) { - _contentItem->path = path->currentPath(); - } - - if (!shadings.empty()) { - for (int i = 0; i < shadings.size(); i++) { - auto &shadingVariant = shadings[i]; - - if (!(shadingVariant.fill || shadingVariant.stroke)) { - continue; - } - - _contentItem->drawContentCount++; - - auto itemShadingVariant = std::make_shared(); - if (shadingVariant.fill) { - itemShadingVariant->fill = shadingVariant.fill->fill(); - } - if (shadingVariant.stroke) { - itemShadingVariant->stroke = shadingVariant.stroke->stroke(); - } - itemShadingVariant->subItemLimit = shadingVariant.subItemLimit; - - _contentItem->shadings.push_back(itemShadingVariant); - } - } - - if (isGroup && !subItems.empty()) { - std::vector> subItemNodes; - for (const auto &subItem : subItems) { - subItem->initializeRenderChildren(); - _contentItem->drawContentCount += subItem->_contentItem->drawContentCount; - _contentItem->subItems.push_back(subItem->_contentItem); - } - } - } - - public: - void updateFrame(AnimationFrameTime frameTime, BezierPathsBoundingBoxContext &boundingBoxContext) { - if (transform) { - transform->update(frameTime); - } - - if (path) { - path->update(frameTime); - } - for (const auto &trim : trims) { - trim->update(frameTime); - } - - for (const auto &shadingVariant : shadings) { - if (shadingVariant.fill) { - shadingVariant.fill->update(frameTime); - } - if (shadingVariant.stroke) { - shadingVariant.stroke->update(frameTime); - } - } - - for (const auto &subItem : subItems) { - subItem->updateFrame(frameTime, boundingBoxContext); - } - } - - bool hasTrims() { - if (!trims.empty()) { - return true; - } - - for (const auto &subItem : subItems) { - if (subItem->hasTrims()) { - return true; - } - } - - return false; - } - - void updateContents(std::optional parentTrim) { - Transform2D containerTransform = Transform2D::identity(); - float containerOpacity = 1.0; - if (transform) { - containerTransform = transform->transform(); - containerOpacity = transform->opacity(); - } - _contentItem->transform = containerTransform; - _contentItem->alpha = containerOpacity; - - if (!trims.empty()) { - _contentItem->trimParams = trims[0]->trimParams(); - } - - for (int i = 0; i < shadings.size(); i++) { - const auto &shadingVariant = shadings[i]; - - if (!(shadingVariant.fill || shadingVariant.stroke)) { - continue; - } - - //std::optional currentTrim = parentTrim; - //TODO:investigate - /*if (!trims.empty()) { - currentTrim = trims[0]; - }*/ - - if (parentTrim) { - CompoundBezierPath compoundPath; - auto paths = collectPaths(shadingVariant.subItemLimit, Transform2D::identity(), true); - for (const auto &path : paths) { - compoundPath.appendPath(path.path.copyUsingTransform(path.transform)); - } - - compoundPath = trimCompoundPath(compoundPath, parentTrim->start, parentTrim->end, parentTrim->offset, parentTrim->type); - - std::vector resultPaths; - for (const auto &path : compoundPath.paths) { - resultPaths.push_back(path); - } - _contentItem->shadings[i]->explicitPath = resultPaths; - } else { - if (hasTrims()) { - CompoundBezierPath compoundPath; - auto paths = collectPaths(shadingVariant.subItemLimit, Transform2D::identity(), true); - for (const auto &path : paths) { - compoundPath.appendPath(path.path.copyUsingTransform(path.transform)); - } - std::vector resultPaths; - for (const auto &path : compoundPath.paths) { - resultPaths.push_back(path); - } - - _contentItem->shadings[i]->explicitPath = resultPaths; - } else { - _contentItem->shadings[i]->explicitPath = std::nullopt; - } - } - } - - if (isGroup && !subItems.empty()) { - for (int i = (int)subItems.size() - 1; i >= 0; i--) { - std::optional childTrim = parentTrim; - for (const auto &trim : trims) { - //TODO:allow combination - //assert(!parentTrim); - childTrim = trim->trimParams(); - } - - subItems[i]->updateContents(childTrim); - } - } - } - }; - -public: - ShapeLayerPresentationTree(std::vector> const &items) { - itemTree = std::make_shared(); - itemTree->isGroup = true; - ShapeLayerPresentationTree::renderTreeContent(items, itemTree); - } - - ShapeLayerPresentationTree(std::shared_ptr const &solidLayer) { - itemTree = std::make_shared(); - itemTree->isGroup = true; - - std::vector> items; - items.push_back(std::make_shared( - std::nullopt, - std::nullopt, - std::nullopt, - std::nullopt, - solidLayer->hidden, - std::nullopt, - std::nullopt, - std::nullopt, - std::nullopt, - KeyframeGroup(Vector3D(0.0, 0.0, 0.0)), - KeyframeGroup(Vector3D(solidLayer->width, solidLayer->height, 0.0)), - KeyframeGroup(Vector1D(0.0)) - )); - ShapeLayerPresentationTree::renderTreeContent(items, itemTree); - } - - virtual ~ShapeLayerPresentationTree() = default; - -private: - static void renderTreeContent(std::vector> const &items, std::shared_ptr &itemTree) { - for (const auto &item : items) { - if (item->hidden()) { - continue; - } - - switch (item->type) { - case ShapeType::Fill: { - Fill const &fill = *((Fill *)item.get()); - - itemTree->addFill(std::make_shared(fill)); - - break; - } - case ShapeType::GradientFill: { - GradientFill const &gradientFill = *((GradientFill *)item.get()); - - itemTree->addFill(std::make_shared(gradientFill)); - - break; - } - case ShapeType::Stroke: { - Stroke const &stroke = *((Stroke *)item.get()); - - itemTree->addStroke(std::make_shared(stroke)); - - break; - } - case ShapeType::GradientStroke: { - GradientStroke const &gradientStroke = *((GradientStroke *)item.get()); - - itemTree->addStroke(std::make_shared(gradientStroke)); - - break; - } - case ShapeType::Group: { - Group const &group = *((Group *)item.get()); - - auto groupItem = std::make_shared(); - groupItem->isGroup = true; - - ShapeLayerPresentationTree::renderTreeContent(group.items, groupItem); - - itemTree->addSubItem(groupItem); - - break; - } - case ShapeType::Shape: { - Shape const &shape = *((Shape *)item.get()); - - auto shapeItem = std::make_shared(); - shapeItem->setPath(std::make_unique(shape)); - itemTree->addSubItem(shapeItem); - - break; - } - case ShapeType::Trim: { - Trim const &trim = *((Trim *)item.get()); - - auto groupItem = std::make_shared(); - groupItem->isGroup = true; - for (const auto &subItem : itemTree->subItems) { - groupItem->addSubItem(subItem); - } - groupItem->addTrim(trim); - itemTree->subItems.clear(); - itemTree->addSubItem(groupItem); - - break; - } - case ShapeType::Transform: { - auto transform = std::static_pointer_cast(item); - - itemTree->setTransform(std::make_unique(transform)); - - break; - } - case ShapeType::Ellipse: { - Ellipse const &ellipse = *((Ellipse *)item.get()); - - auto shapeItem = std::make_shared(); - shapeItem->setPath(std::make_unique(ellipse)); - itemTree->addSubItem(shapeItem); - - break; - } - case ShapeType::Merge: { - //assert(false); - break; - } - case ShapeType::Rectangle: { - Rectangle const &rectangle = *((Rectangle *)item.get()); - - auto shapeItem = std::make_shared(); - shapeItem->setPath(std::make_unique(rectangle)); - itemTree->addSubItem(shapeItem); - - break; - } - case ShapeType::Repeater: { - assert(false); - break; - } - case ShapeType::Star: { - Star const &star = *((Star *)item.get()); - - auto shapeItem = std::make_shared(); - shapeItem->setPath(std::make_unique(star)); - itemTree->addSubItem(shapeItem); - - break; - } - case ShapeType::RoundedRectangle: { - //TODO:restore - break; - } - default: { - break; - } - } - } - - itemTree->initializeRenderChildren(); - } - -public: - std::shared_ptr itemTree; -}; - -ShapeCompositionLayer::ShapeCompositionLayer(std::shared_ptr const &shapeLayer) : -CompositionLayer(shapeLayer, Vector2D::Zero()) { - _contentTree = std::make_shared(shapeLayer->items); -} - -ShapeCompositionLayer::ShapeCompositionLayer(std::shared_ptr const &solidLayer) : -CompositionLayer(solidLayer, Vector2D::Zero()) { - _contentTree = std::make_shared(solidLayer); -} - -void ShapeCompositionLayer::displayContentsWithFrame(float frame, bool forceUpdates, BezierPathsBoundingBoxContext &boundingBoxContext) { - _frameTime = frame; - _frameTimeInitialized = true; - _contentTree->itemTree->updateFrame(_frameTime, boundingBoxContext); - _contentTree->itemTree->updateContents(std::nullopt); -} - -std::shared_ptr ShapeCompositionLayer::renderTreeNode(BezierPathsBoundingBoxContext &boundingBoxContext) { - if (!_frameTimeInitialized) { - _frameTime = 0.0; - _frameTimeInitialized = true; - _contentTree->itemTree->updateFrame(_frameTime, boundingBoxContext); - _contentTree->itemTree->updateContents(std::nullopt); - } - - if (!_renderTreeNode) { - _contentRenderTreeNode = std::make_shared( - Vector2D(0.0, 0.0), - Transform2D::identity(), - 1.0, - false, - false, - std::vector>(), - nullptr, - false - ); - _contentRenderTreeNode->_contentItem = _contentTree->itemTree->_contentItem; - _contentRenderTreeNode->drawContentCount = _contentTree->itemTree->_contentItem->drawContentCount; - - std::vector> subnodes; - subnodes.push_back(_contentRenderTreeNode); - - std::shared_ptr maskNode; - bool invertMask = false; - if (_matteLayer) { - maskNode = _matteLayer->renderTreeNode(boundingBoxContext); - if (maskNode && _matteType.has_value() && _matteType.value() == MatteType::Invert) { - invertMask = true; - } - } - - _renderTreeNode = std::make_shared( - Vector2D(0.0, 0.0), - Transform2D::identity(), - 1.0, - false, - false, - subnodes, - maskNode, - invertMask - ); - } - - _contentRenderTreeNode->_size = _contentsLayer->size(); - _contentRenderTreeNode->_masksToBounds = _contentsLayer->masksToBounds(); - - _renderTreeNode->_masksToBounds = masksToBounds(); - - _renderTreeNode->_size = size(); - - return _renderTreeNode; -} - -void ShapeCompositionLayer::updateContentsLayerParameters() { - _contentRenderTreeNode->_transform = _contentsLayer->transform(); - _contentRenderTreeNode->_alpha = _contentsLayer->opacity(); - _contentRenderTreeNode->_isHidden = _contentsLayer->isHidden(); -} - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp deleted file mode 100644 index 8aff5c77a6b..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef ShapeCompositionLayer_hpp -#define ShapeCompositionLayer_hpp - -#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp" -#include "Lottie/Private/Model/Layers/ShapeLayerModel.hpp" -#include "Lottie/Private/Model/Layers/SolidLayerModel.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp" - -namespace lottie { - -class ShapeLayerPresentationTree; - -/// A CompositionLayer responsible for initializing and rendering shapes -class ShapeCompositionLayer: public CompositionLayer { -public: - ShapeCompositionLayer(std::shared_ptr const &shapeLayer); - ShapeCompositionLayer(std::shared_ptr const &solidLayer); - - virtual void displayContentsWithFrame(float frame, bool forceUpdates, BezierPathsBoundingBoxContext &boundingBoxContext) override; - virtual std::shared_ptr renderTreeNode(BezierPathsBoundingBoxContext &boundingBoxContext) override; - void initializeContentsLayerParameters(); - virtual void updateContentsLayerParameters() override; - -private: - std::shared_ptr _contentTree; - - AnimationFrameTime _frameTime = 0.0; - bool _frameTimeInitialized = false; - - std::shared_ptr _renderTreeNode; - std::shared_ptr _contentRenderTreeNode; -}; - -} - -#endif /* ShapeCompositionLayer_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeUtils/BezierPathUtils.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeUtils/BezierPathUtils.cpp deleted file mode 100644 index 71fc3cc1d75..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeUtils/BezierPathUtils.cpp +++ /dev/null @@ -1,487 +0,0 @@ -#include "BezierPathUtils.hpp" - -namespace lottie { - -BezierPath makeEllipseBezierPath( - Vector2D const &size, - Vector2D const ¢er, - PathDirection direction -) { - const float ControlPointConstant = 0.55228; - - Vector2D half = size * 0.5; - if (direction == PathDirection::CounterClockwise) { - half.x = half.x * -1.0; - } - - Vector2D q1(center.x, center.y - half.y); - Vector2D q2(center.x + half.x, center.y); - Vector2D q3(center.x, center.y + half.y); - Vector2D q4(center.x - half.x, center.y); - - Vector2D cp = half * ControlPointConstant; - - BezierPath path(CurveVertex::relative( - q1, - Vector2D(-cp.x, 0), - Vector2D(cp.x, 0))); - path.addVertex(CurveVertex::relative( - q2, - Vector2D(0, -cp.y), - Vector2D(0, cp.y))); - - path.addVertex(CurveVertex::relative( - q3, - Vector2D(cp.x, 0), - Vector2D(-cp.x, 0))); - - path.addVertex(CurveVertex::relative( - q4, - Vector2D(0, cp.y), - Vector2D(0, -cp.y))); - - path.addVertex(CurveVertex::relative( - q1, - Vector2D(-cp.x, 0), - Vector2D(cp.x, 0))); - path.close(); - return path; -} - -BezierPath makeRectangleBezierPath( - Vector2D const &position, - Vector2D const &inputSize, - float cornerRadius, - PathDirection direction -) { - const float ControlPointConstant = 0.55228; - - Vector2D size = inputSize * 0.5; - float radius = std::min(std::min(cornerRadius, (float)size.x), (float)size.y); - - BezierPath bezierPath; - std::vector points; - - if (radius <= 0.0) { - /// No Corners - points = { - /// Lead In - CurveVertex::relative( - Vector2D(size.x, -size.y), - Vector2D::Zero(), - Vector2D::Zero()) - .translated(position), - /// Corner 1 - CurveVertex::relative( - Vector2D(size.x, size.y), - Vector2D::Zero(), - Vector2D::Zero()) - .translated(position), - /// Corner 2 - CurveVertex::relative( - Vector2D(-size.x, size.y), - Vector2D::Zero(), - Vector2D::Zero()) - .translated(position), - /// Corner 3 - CurveVertex::relative( - Vector2D(-size.x, -size.y), - Vector2D::Zero(), - Vector2D::Zero()) - .translated(position), - /// Corner 4 - CurveVertex::relative( - Vector2D(size.x, -size.y), - Vector2D::Zero(), - Vector2D::Zero()) - .translated(position) - }; - } else { - float controlPoint = radius * ControlPointConstant; - points = { - /// Lead In - CurveVertex::absolute( - Vector2D(radius, 0), - Vector2D(radius, 0), - Vector2D(radius, 0)) - .translated(Vector2D(-radius, radius)) - .translated(Vector2D(size.x, -size.y)) - .translated(position), - /// Corner 1 - CurveVertex::absolute( - Vector2D(radius, 0), // Point - Vector2D(radius, 0), // In tangent - Vector2D(radius, controlPoint)) - .translated(Vector2D(-radius, -radius)) - .translated(Vector2D(size.x, size.y)) - .translated(position), - CurveVertex::absolute( - Vector2D(0, radius), // Point - Vector2D(controlPoint, radius), // In tangent - Vector2D(0, radius)) // Out Tangent - .translated(Vector2D(-radius, -radius)) - .translated(Vector2D(size.x, size.y)) - .translated(position), - /// Corner 2 - CurveVertex::absolute( - Vector2D(0, radius), // Point - Vector2D(0, radius), // In tangent - Vector2D(-controlPoint, radius))// Out tangent - .translated(Vector2D(radius, -radius)) - .translated(Vector2D(-size.x, size.y)) - .translated(position), - CurveVertex::absolute( - Vector2D(-radius, 0), // Point - Vector2D(-radius, controlPoint), // In tangent - Vector2D(-radius, 0)) // Out tangent - .translated(Vector2D(radius, -radius)) - .translated(Vector2D(-size.x, size.y)) - .translated(position), - /// Corner 3 - CurveVertex::absolute( - Vector2D(-radius, 0), // Point - Vector2D(-radius, 0), // In tangent - Vector2D(-radius, -controlPoint)) // Out tangent - .translated(Vector2D(radius, radius)) - .translated(Vector2D(-size.x, -size.y)) - .translated(position), - CurveVertex::absolute( - Vector2D(0, -radius), // Point - Vector2D(-controlPoint, -radius), // In tangent - Vector2D(0, -radius)) // Out tangent - .translated(Vector2D(radius, radius)) - .translated(Vector2D(-size.x, -size.y)) - .translated(position), - /// Corner 4 - CurveVertex::absolute( - Vector2D(0, -radius), // Point - Vector2D(0, -radius), // In tangent - Vector2D(controlPoint, -radius)) // Out tangent - .translated(Vector2D(-radius, radius)) - .translated(Vector2D(size.x, -size.y)) - .translated(position), - CurveVertex::absolute( - Vector2D(radius, 0), // Point - Vector2D(radius, -controlPoint), // In tangent - Vector2D(radius, 0)) // Out tangent - .translated(Vector2D(-radius, radius)) - .translated(Vector2D(size.x, -size.y)) - .translated(position) - }; - } - bool reversed = direction == PathDirection::CounterClockwise; - if (reversed) { - for (auto vertexIt = points.rbegin(); vertexIt != points.rend(); vertexIt++) { - bezierPath.addVertex((*vertexIt).reversed()); - } - } else { - for (auto vertexIt = points.begin(); vertexIt != points.end(); vertexIt++) { - bezierPath.addVertex(*vertexIt); - } - } - bezierPath.close(); - return bezierPath; -} - -/// Magic number needed for building path data -static constexpr float StarNodePolystarConstant = 0.47829; - -BezierPath makeStarBezierPath( - Vector2D const &position, - float outerRadius, - float innerRadius, - float inputOuterRoundedness, - float inputInnerRoundedness, - float numberOfPoints, - float rotation, - PathDirection direction -) { - float currentAngle = degreesToRadians(rotation - 90.0); - float anglePerPoint = (2.0 * M_PI) / numberOfPoints; - float halfAnglePerPoint = anglePerPoint / 2.0; - float partialPointAmount = numberOfPoints - floor(numberOfPoints); - float outerRoundedness = inputOuterRoundedness * 0.01; - float innerRoundedness = inputInnerRoundedness * 0.01; - - Vector2D point = Vector2D::Zero(); - - float partialPointRadius = 0.0; - if (partialPointAmount != 0.0) { - currentAngle += halfAnglePerPoint * (1 - partialPointAmount); - partialPointRadius = innerRadius + partialPointAmount * (outerRadius - innerRadius); - point.x = (partialPointRadius * cos(currentAngle)); - point.y = (partialPointRadius * sin(currentAngle)); - currentAngle += anglePerPoint * partialPointAmount / 2; - } else { - point.x = (outerRadius * cos(currentAngle)); - point.y = (outerRadius * sin(currentAngle)); - currentAngle += halfAnglePerPoint; - } - - std::vector vertices; - vertices.push_back(CurveVertex::relative(point + position, Vector2D::Zero(), Vector2D::Zero())); - - Vector2D previousPoint = point; - bool longSegment = false; - int numPoints = (int)(ceil(numberOfPoints) * 2.0); - for (int i = 0; i < numPoints; i++) { - float radius = longSegment ? outerRadius : innerRadius; - float dTheta = halfAnglePerPoint; - if (partialPointRadius != 0.0 && i == numPoints - 2) { - dTheta = anglePerPoint * partialPointAmount / 2; - } - if (partialPointRadius != 0.0 && i == numPoints - 1) { - radius = partialPointRadius; - } - previousPoint = point; - point.x = (radius * cos(currentAngle)); - point.y = (radius * sin(currentAngle)); - - if (innerRoundedness == 0.0 && outerRoundedness == 0.0) { - vertices.push_back(CurveVertex::relative(point + position, Vector2D::Zero(), Vector2D::Zero())); - } else { - float cp1Theta = (atan2(previousPoint.y, previousPoint.x) - M_PI / 2.0); - float cp1Dx = cos(cp1Theta); - float cp1Dy = sin(cp1Theta); - - float cp2Theta = (atan2(point.y, point.x) - M_PI / 2.0); - float cp2Dx = cos(cp2Theta); - float cp2Dy = sin(cp2Theta); - - float cp1Roundedness = longSegment ? innerRoundedness : outerRoundedness; - float cp2Roundedness = longSegment ? outerRoundedness : innerRoundedness; - float cp1Radius = longSegment ? innerRadius : outerRadius; - float cp2Radius = longSegment ? outerRadius : innerRadius; - - Vector2D cp1( - cp1Radius * cp1Roundedness * StarNodePolystarConstant * cp1Dx, - cp1Radius * cp1Roundedness * StarNodePolystarConstant * cp1Dy - ); - Vector2D cp2( - cp2Radius * cp2Roundedness * StarNodePolystarConstant * cp2Dx, - cp2Radius * cp2Roundedness * StarNodePolystarConstant * cp2Dy - ); - if (partialPointAmount != 0.0) { - if (i == 0) { - cp1 = cp1 * partialPointAmount; - } else if (i == numPoints - 1) { - cp2 = cp2 * partialPointAmount; - } - } - auto previousVertex = vertices[vertices.size() - 1]; - vertices[vertices.size() - 1] = CurveVertex::absolute( - previousVertex.point, - previousVertex.inTangent, - previousVertex.point - cp1 - ); - vertices.push_back(CurveVertex::relative(point + position, cp2, Vector2D::Zero())); - } - currentAngle += dTheta; - longSegment = !longSegment; - } - - bool reverse = direction == PathDirection::CounterClockwise; - BezierPath path; - if (reverse) { - for (auto vertexIt = vertices.rbegin(); vertexIt != vertices.rend(); vertexIt++) { - path.addVertex((*vertexIt).reversed()); - } - } else { - for (auto vertexIt = vertices.begin(); vertexIt != vertices.end(); vertexIt++) { - path.addVertex(*vertexIt); - } - } - path.close(); - return path; -} - -CompoundBezierPath trimCompoundPath(CompoundBezierPath sourcePath, float start, float end, float offset, TrimType type) { - /// No need to trim, it's a full path - if (start == 0.0 && end == 1.0) { - return sourcePath; - } - - /// All paths are empty. - if (start == end) { - return CompoundBezierPath(); - } - - if (type == TrimType::Simultaneously) { - CompoundBezierPath result; - - for (BezierPath &path : sourcePath.paths) { - CompoundBezierPath tempPath; - tempPath.appendPath(path); - - auto subPaths = tempPath.trim(start, end, offset); - - for (const auto &subPath : subPaths->paths) { - result.appendPath(subPath); - } - } - - return result; - } - - /// Individual path trimming. - - /// Brace yourself for the below code. - - /// Normalize lengths with offset. - float startPosition = fmod(start + offset, 1.0); - float endPosition = fmod(end + offset, 1.0); - - if (startPosition < 0.0) { - startPosition = 1.0 + startPosition; - } - - if (endPosition < 0.0) { - endPosition = 1.0 + endPosition; - } - if (startPosition == 1.0) { - startPosition = 0.0; - } - if (endPosition == 0.0) { - endPosition = 1.0; - } - - /// First get the total length of all paths. - float totalLength = 0.0; - for (auto &upstreamPath : sourcePath.paths) { - totalLength += upstreamPath.length(); - } - - /// Now determine the start and end cut lengths - float startLength = startPosition * totalLength; - float endLength = endPosition * totalLength; - float pathStart = 0.0; - - CompoundBezierPath result; - - /// Now loop through all path containers - for (auto &pathContainer : sourcePath.paths) { - auto pathEnd = pathStart + pathContainer.length(); - - if (!isInRange(startLength, pathStart, pathEnd) && - isInRange(endLength, pathStart, pathEnd)) { - // pathStart|=======E----------------------|pathEnd - // Cut path components, removing after end. - - float pathCutLength = endLength - pathStart; - float subpathStart = 0.0; - float subpathEnd = subpathStart + pathContainer.length(); - if (pathCutLength < subpathEnd) { - /// This is the subpath that needs to be cut. - float cutLength = pathCutLength - subpathStart; - - CompoundBezierPath tempPath; - tempPath.appendPath(pathContainer); - auto newPaths = tempPath.trim(0, cutLength / pathContainer.length(), 0); - for (const auto &newPath : newPaths->paths) { - result.appendPath(newPath); - } - } else { - /// Add to container and move on - result.appendPath(pathContainer); - } - /*if (pathCutLength == subpathEnd) { - /// Right on the end. The next subpath is not included. Break. - break; - } - subpathStart = subpathEnd;*/ - } else if (!isInRange(endLength, pathStart, pathEnd) && - isInRange(startLength, pathStart, pathEnd)) { - // pathStart|-------S======================|pathEnd - // - - // Cut path components, removing before beginning. - float pathCutLength = startLength - pathStart; - // Clear paths from container - float subpathStart = 0.0; - float subpathEnd = subpathStart + pathContainer.length(); - - if (subpathStart < pathCutLength && pathCutLength < subpathEnd) { - /// This is the subpath that needs to be cut. - float cutLength = pathCutLength - subpathStart; - CompoundBezierPath tempPath; - tempPath.appendPath(pathContainer); - auto newPaths = tempPath.trim(cutLength / pathContainer.length(), 1, 0); - for (const auto &newPath : newPaths->paths) { - result.appendPath(newPath); - } - } else if (pathCutLength <= subpathStart) { - result.appendPath(pathContainer); - } - //subpathStart = subpathEnd; - } else if (isInRange(endLength, pathStart, pathEnd) && - isInRange(startLength, pathStart, pathEnd)) { - // pathStart|-------S============E---------|endLength - // pathStart|=====E----------------S=======|endLength - // trim from path beginning to endLength. - - // Cut path components, removing before beginnings. - float startCutLength = startLength - pathStart; - float endCutLength = endLength - pathStart; - - float subpathStart = 0.0; - - float subpathEnd = subpathStart + pathContainer.length(); - - if (!isInRange(startCutLength, subpathStart, subpathEnd) && - !isInRange(endCutLength, subpathStart, subpathEnd)) - { - // The whole path is included. Add - // S|==============================|E - result.appendPath(pathContainer); - } else if (isInRange(startCutLength, subpathStart, subpathEnd) && - !isInRange(endCutLength, subpathStart, subpathEnd)) { - /// The start of the path needs to be trimmed - // |-------S======================|E - float cutLength = startCutLength - subpathStart; - CompoundBezierPath tempPath; - tempPath.appendPath(pathContainer); - auto newPaths = tempPath.trim(cutLength / pathContainer.length(), 1, 0); - for (const auto &newPath : newPaths->paths) { - result.appendPath(newPath); - } - } else if (!isInRange(startCutLength, subpathStart, subpathEnd) && - isInRange(endCutLength, subpathStart, subpathEnd)) { - // S|=======E----------------------| - float cutLength = endCutLength - subpathStart; - CompoundBezierPath tempPath; - tempPath.appendPath(pathContainer); - auto newPaths = tempPath.trim(0, cutLength / pathContainer.length(), 0); - for (const auto &newPath : newPaths->paths) { - result.appendPath(newPath); - } - } else if (isInRange(startCutLength, subpathStart, subpathEnd) && - isInRange(endCutLength, subpathStart, subpathEnd)) { - // |-------S============E---------| - float cutFromLength = startCutLength - subpathStart; - float cutToLength = endCutLength - subpathStart; - CompoundBezierPath tempPath; - tempPath.appendPath(pathContainer); - auto newPaths = tempPath.trim( - cutFromLength / pathContainer.length(), - cutToLength / pathContainer.length(), - 0 - ); - for (const auto &newPath : newPaths->paths) { - result.appendPath(newPath); - } - } - } else if ((endLength <= pathStart && pathEnd <= startLength) || - (startLength <= pathStart && endLength <= pathStart) || - (pathEnd <= startLength && pathEnd <= endLength)) { - /// The Path needs to be cleared - } else { - result.appendPath(pathContainer); - } - - pathStart = pathEnd; - } - - return result; -} - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeUtils/BezierPathUtils.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeUtils/BezierPathUtils.hpp deleted file mode 100644 index 677dbe96bd1..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeUtils/BezierPathUtils.hpp +++ /dev/null @@ -1,39 +0,0 @@ -#ifndef BezierPaths_h -#define BezierPaths_h - -#include "Lottie/Private/Model/ShapeItems/Ellipse.hpp" -#include -#include "Lottie/Private/Utility/Primitives/CompoundBezierPath.hpp" -#include "Lottie/Private/Model/ShapeItems/Trim.hpp" - -namespace lottie { - -BezierPath makeEllipseBezierPath( - Vector2D const &size, - Vector2D const ¢er, - PathDirection direction -); - -BezierPath makeRectangleBezierPath( - Vector2D const &position, - Vector2D const &inputSize, - float cornerRadius, - PathDirection direction -); - -BezierPath makeStarBezierPath( - Vector2D const &position, - float outerRadius, - float innerRadius, - float inputOuterRoundedness, - float inputInnerRoundedness, - float numberOfPoints, - float rotation, - PathDirection direction -); - -CompoundBezierPath trimCompoundPath(CompoundBezierPath sourcePath, float start, float end, float offset, TrimType type); - -} - -#endif /* BezierPaths_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.cpp deleted file mode 100644 index 5e773d361ba..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "TextCompositionLayer.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp deleted file mode 100644 index 98d7edbbc24..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp +++ /dev/null @@ -1,81 +0,0 @@ -#ifndef TextCompositionLayer_hpp -#define TextCompositionLayer_hpp - -#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp" -#include "Lottie/Private/Model/Layers/TextLayerModel.hpp" -#include "Lottie/Public/TextProvider/AnimationTextProvider.hpp" -#include "Lottie/Public/FontProvider/AnimationFontProvider.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.hpp" - -namespace lottie { - -class TextCompositionLayer: public CompositionLayer { -public: - TextCompositionLayer(std::shared_ptr const &textLayer, std::shared_ptr textProvider, std::shared_ptr fontProvider) : - CompositionLayer(textLayer, Vector2D::Zero()) { - std::shared_ptr rootNode; - for (const auto &animator : textLayer->animators) { - rootNode = std::make_shared(rootNode, animator); - } - _rootNode = rootNode; - _textDocument = std::make_shared>(textLayer->text.keyframes); - - _textProvider = textProvider; - _fontProvider = fontProvider; - - if (_rootNode) { - _childKeypaths.push_back(rootNode); - } - } - - std::shared_ptr const &textProvider() const { - return _textProvider; - } - void setTextProvider(std::shared_ptr const &textProvider) { - _textProvider = textProvider; - } - - std::shared_ptr const &fontProvider() const { - return _fontProvider; - } - void setFontProvider(std::shared_ptr const &fontProvider) { - _fontProvider = fontProvider; - } - - virtual void displayContentsWithFrame(float frame, bool forceUpdates, BezierPathsBoundingBoxContext &boundingBoxContext) override { - if (!_textDocument) { - return; - } - - bool documentUpdate = _textDocument->hasUpdate(frame); - - bool animatorUpdate = false; - if (_rootNode) { - animatorUpdate = _rootNode->updateContents(frame, forceUpdates); - } - - if (!(documentUpdate || animatorUpdate)) { - return; - } - - if (_rootNode) { - _rootNode->rebuildOutputs(frame); - } - } - -public: - virtual bool isTextCompositionLayer() const override { - return true; - } - -private: - std::shared_ptr _rootNode; - std::shared_ptr> _textDocument; - - std::shared_ptr _textProvider; - std::shared_ptr _fontProvider; -}; - -} - -#endif /* TextCompositionLayer_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.cpp deleted file mode 100644 index a205f16eba9..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "MainThreadAnimationLayer.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.hpp deleted file mode 100644 index b9e47a7a602..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.hpp +++ /dev/null @@ -1,279 +0,0 @@ -#ifndef MainThreadAnimationLayer_hpp -#define MainThreadAnimationLayer_hpp - -#include "Lottie/Public/Primitives/CALayer.hpp" -#include "Lottie/Public/ImageProvider/AnimationImageProvider.hpp" -#include "Lottie/Private/Model/Animation.hpp" -#include "Lottie/Public/TextProvider/AnimationTextProvider.hpp" -#include "Lottie/Public/FontProvider/AnimationFontProvider.hpp" -#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerImageProvider.hpp" -#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerTextProvider.hpp" -#include "Lottie/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.hpp" -#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerFontProvider.hpp" -#include "Lottie/Public/DynamicProperties/AnyValueProvider.hpp" -#include "Lottie/Public/DynamicProperties/AnimationKeypath.hpp" - -namespace lottie { - -class BlankImageProvider: public AnimationImageProvider { -public: - virtual ~BlankImageProvider() = default; - - std::shared_ptr imageForAsset(ImageAsset const &asset) { - return nullptr; - } -}; - -class MainThreadAnimationLayer: public CALayer { -public: - MainThreadAnimationLayer( - Animation const &animation, - std::shared_ptr const &imageProvider, - std::shared_ptr const &textProvider, - std::shared_ptr const &fontProvider - ) { - if (animation.assetLibrary) { - _layerImageProvider = std::make_shared(imageProvider, animation.assetLibrary->imageAssets); - } else { - std::map> imageAssets; - _layerImageProvider = std::make_shared(imageProvider, imageAssets); - } - - _layerTextProvider = std::make_shared(textProvider); - _layerFontProvider = std::make_shared(fontProvider); - - setSize(Vector2D(animation.width, animation.height)); - - auto layers = initializeCompositionLayers( - animation.layers, - animation.assetLibrary, - _layerImageProvider, - textProvider, - fontProvider, - animation.framerate - ); - - std::vector> imageLayers; - std::vector> textLayers; - - std::shared_ptr mattedLayer; - - for (auto layerIt = layers.rbegin(); layerIt != layers.rend(); layerIt++) { - std::shared_ptr const &layer = *layerIt; - layer->setSize(size()); - _animationLayers.push_back(layer); - - if (layer->isImageCompositionLayer()) { - imageLayers.push_back(std::static_pointer_cast(layer)); - } - if (layer->isTextCompositionLayer()) { - textLayers.push_back(std::static_pointer_cast(layer)); - } - - if (mattedLayer) { - /// The previous layer requires this layer to be its matte - mattedLayer->setMatteLayer(layer); - mattedLayer = nullptr; - continue; - } - if (layer->matteType().has_value() && (layer->matteType() == MatteType::Add || layer->matteType() == MatteType::Invert)) { - /// We have a layer that requires a matte. - mattedLayer = layer; - } - addSublayer(layer); - } - - _layerImageProvider->addImageLayers(imageLayers); - _layerImageProvider->reloadImages(); - _layerTextProvider->addTextLayers(textLayers); - _layerTextProvider->reloadTexts(); - _layerFontProvider->addTextLayers(textLayers); - _layerFontProvider->reloadTexts(); - - renderTreeNode(); - } - - void setRespectAnimationFrameRate(bool respectAnimationFrameRate) { - _respectAnimationFrameRate = respectAnimationFrameRate; - } - - void display() { - float newFrame = currentFrame(); - if (_respectAnimationFrameRate) { - newFrame = floor(newFrame); - } - for (const auto &layer : _animationLayers) { - layer->displayWithFrame(newFrame, false, _boundingBoxContext); - } - } - - std::vector> const &animationLayers() const { - return _animationLayers; - } - - void reloadImages() { - _layerImageProvider->reloadImages(); - } - - /// Forces the view to update its drawing. - void forceDisplayUpdate() { - for (const auto &layer : _animationLayers) { - layer->displayWithFrame(currentFrame(), true, _boundingBoxContext); - } - } - - void logHierarchyKeypaths() { - printf("Lottie: Logging Animation Keypaths\n"); - assert(false); - //animationLayers.forEach({ $0.logKeypaths(for: nil) }) - } - - void setValueProvider(std::shared_ptr const &valueProvider, AnimationKeypath const &keypath) { - /*for (const auto &layer : _animationLayers) { - assert(false); - if let foundProperties = layer.nodeProperties(for: keypath) { - for property in foundProperties { - property.setProvider(provider: valueProvider) - } - layer.displayWithFrame(frame: presentation()?.currentFrame ?? currentFrame, forceUpdates: true) - } - }*/ - } - - std::optional getValue(AnimationKeypath const &keypath, std::optional atFrame) { - /*for (const auto &layer : _animationLayers) { - assert(false); - if - let foundProperties = layer.nodeProperties(for: keypath), - let first = foundProperties.first - { - return first.valueProvider.value(frame: atFrame ?? currentFrame) - } - }*/ - return std::nullopt; - } - - std::optional getOriginalValue(AnimationKeypath const &keypath, std::optional atFrame) { - /*for (const auto &layer : _animationLayers) { - assert(false); - if - let foundProperties = layer.nodeProperties(for: keypath), - let first = foundProperties.first - { - return first.originalValueProvider.value(frame: atFrame ?? currentFrame) - } - }*/ - return std::nullopt; - } - - std::shared_ptr layerForKeypath(AnimationKeypath const &keyPath) { - assert(false); - /*for layer in animationLayers { - if let foundLayer = layer.layer(for: keypath) { - return foundLayer - } - }*/ - return nullptr; - } - - std::vector> animatorNodesForKeypath(AnimationKeypath const &keypath) { - std::vector> results; - /*for (const auto &layer : _animationLayers) { - if let nodes = layer.animatorNodes(for: keypath) { - results.append(contentsOf: nodes) - } - }*/ - return results; - } - - float currentFrame() const { - return _currentFrame; - } - void setCurrentFrame(float currentFrame) { - _currentFrame = currentFrame; - - for (size_t i = 0; i < _animationLayers.size(); i++) { - _animationLayers[i]->displayWithFrame(_currentFrame, false, _boundingBoxContext); - } - } - - std::shared_ptr imageProvider() const { - return _layerImageProvider->imageProvider(); - } - void setImageProvider(std::shared_ptr const &imageProvider) { - _layerImageProvider->setImageProvider(imageProvider); - } - - std::shared_ptr textProvider() const { - return _layerTextProvider->textProvider(); - } - void setTextProvider(std::shared_ptr const &textProvider) { - _layerTextProvider->setTextProvider(textProvider); - } - - std::shared_ptr fontProvider() const { - return _layerFontProvider->fontProvider(); - } - void setFontProvider(std::shared_ptr const &fontProvider) { - _layerFontProvider->setFontProvider(fontProvider); - } - - virtual std::shared_ptr renderTreeNode() { - if (!_renderTreeNode) { - std::vector> subnodes; - for (const auto &animationLayer : _animationLayers) { - bool found = false; - for (const auto &sublayer : sublayers()) { - if (animationLayer == sublayer) { - found = true; - break; - } - } - if (found) { - auto node = animationLayer->renderTreeNode(_boundingBoxContext); - if (node) { - subnodes.push_back(node); - } - } - } - _renderTreeNode = std::make_shared( - size(), - Transform2D::identity(), - 1.0, - false, - false, - subnodes, - nullptr, - false - ); - } - - return _renderTreeNode; - } - -private: - // MARK: Internal - - /// The animatable Current Frame Property - float _currentFrame = 0.0; - - std::shared_ptr _imageProvider; - std::shared_ptr _textProvider; - std::shared_ptr _fontProvider; - - bool _respectAnimationFrameRate = true; - - std::vector> _animationLayers; - - std::shared_ptr _layerImageProvider; - std::shared_ptr _layerTextProvider; - std::shared_ptr _layerFontProvider; - - std::shared_ptr _renderTreeNode; - - BezierPathsBoundingBoxContext _boundingBoxContext; -}; - -} - -#endif /* MainThreadAnimationLayer_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.cpp deleted file mode 100644 index 8a98548bd3e..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.cpp +++ /dev/null @@ -1,111 +0,0 @@ -#include "CompositionLayersInitializer.hpp" - -#include "Lottie/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.hpp" -#include "Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp" -#include "Lottie/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.hpp" -#include "Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.hpp" -#include "Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp" - -namespace lottie { - -std::vector> initializeCompositionLayers( - std::vector> const &layers, - std::shared_ptr const &assetLibrary, - std::shared_ptr const &layerImageProvider, - std::shared_ptr const &textProvider, - std::shared_ptr const &fontProvider, - float frameRate -) { - std::vector> compositionLayers; - std::map> layerMap; - - std::vector> childLayers; - - for (const auto &layer : layers) { - if (layer->hidden) { - auto genericLayer = std::make_shared(layer); - compositionLayers.push_back(genericLayer); - if (layer->index) { - layerMap.insert(std::make_pair(layer->index.value(), genericLayer)); - } - } else if (layer->type == LayerType::Shape) { - auto shapeContainer = std::make_shared(std::static_pointer_cast(layer)); - compositionLayers.push_back(shapeContainer); - if (layer->index) { - layerMap.insert(std::make_pair(layer->index.value(), shapeContainer)); - } - } else if (layer->type == LayerType::Solid) { - auto shapeContainer = std::make_shared(std::static_pointer_cast(layer)); - compositionLayers.push_back(shapeContainer); - if (layer->index) { - layerMap.insert(std::make_pair(layer->index.value(), shapeContainer)); - } - } else if (layer->type == LayerType::Precomp && assetLibrary) { - auto precompLayer = std::static_pointer_cast(layer); - auto precompAssetIt = assetLibrary->precompAssets.find(precompLayer->referenceID); - if (precompAssetIt != assetLibrary->precompAssets.end()) { - auto precompContainer = std::make_shared( - precompLayer, - *(precompAssetIt->second), - layerImageProvider, - textProvider, - fontProvider, - assetLibrary, - frameRate - ); - compositionLayers.push_back(precompContainer); - if (layer->index) { - layerMap.insert(std::make_pair(layer->index.value(), precompContainer)); - } - } - } else if (layer->type == LayerType::Image && assetLibrary) { - auto imageLayer = std::static_pointer_cast(layer); - auto imageAssetIt = assetLibrary->imageAssets.find(imageLayer->referenceID); - if (imageAssetIt != assetLibrary->imageAssets.end()) { - auto imageContainer = std::make_shared( - imageLayer, - Vector2D((*imageAssetIt->second).width, (*imageAssetIt->second).height) - ); - compositionLayers.push_back(imageContainer); - if (layer->index) { - layerMap.insert(std::make_pair(layer->index.value(), imageContainer)); - } - } - } else if (layer->type == LayerType::Text) { - auto textContainer = std::make_shared(std::static_pointer_cast(layer), textProvider, fontProvider); - compositionLayers.push_back(textContainer); - if (layer->index) { - layerMap.insert(std::make_pair(layer->index.value(), textContainer)); - } - } else { - auto genericLayer = std::make_shared(layer); - compositionLayers.push_back(genericLayer); - if (layer->index) { - layerMap.insert(std::make_pair(layer->index.value(), genericLayer)); - } - } - if (layer->parent) { - childLayers.push_back(layer); - } - } - - /// Now link children with their parents - for (const auto &layerModel : childLayers) { - if (!layerModel->index.has_value()) { - continue; - } - if (const auto parentID = layerModel->parent) { - auto childLayerIt = layerMap.find(layerModel->index.value()); - if (childLayerIt != layerMap.end()) { - auto parentLayerIt = layerMap.find(parentID.value()); - if (parentLayerIt != layerMap.end()) { - childLayerIt->second->transformNode()->setParentNode(parentLayerIt->second->transformNode()); - } - } - } - } - - return compositionLayers; -} - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.hpp deleted file mode 100644 index 74df3529d34..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef CompositionLayersInitializer_hpp -#define CompositionLayersInitializer_hpp - -#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp" -#include "Lottie/Private/Model/Assets/AssetLibrary.hpp" -#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerImageProvider.hpp" -#include "Lottie/Public/TextProvider/AnimationTextProvider.hpp" -#include "Lottie/Public/FontProvider/AnimationFontProvider.hpp" - -namespace lottie { - -std::vector> initializeCompositionLayers( - std::vector> const &layers, - std::shared_ptr const &assetLibrary, - std::shared_ptr const &layerImageProvider, - std::shared_ptr const &textProvider, - std::shared_ptr const &fontProvider, - float frameRate -); - -} - -#endif /* CompositionLayersInitializer_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerFontProvider.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerFontProvider.cpp deleted file mode 100644 index d6123ee219d..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerFontProvider.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "LayerFontProvider.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerFontProvider.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerFontProvider.hpp deleted file mode 100644 index ff9eaae19cf..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerFontProvider.hpp +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef LayerFontProvider_hpp -#define LayerFontProvider_hpp - -#include "Lottie/Public/FontProvider/AnimationFontProvider.hpp" -#include "Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp" - -namespace lottie { - -/// Connects a LottieFontProvider to a group of text layers -class LayerFontProvider { -public: - LayerFontProvider(std::shared_ptr const &fontProvider) { - _fontProvider = fontProvider; - reloadTexts(); - } - - std::shared_ptr const &fontProvider() const { - return _fontProvider; - } - void setFontProvider(std::shared_ptr const &fontProvider) { - _fontProvider = fontProvider; - reloadTexts(); - } - - void addTextLayers(std::vector> const &layers) { - for (const auto &layer : layers) { - _textLayers.push_back(layer); - } - } - - void reloadTexts() { - for (const auto &layer : _textLayers) { - layer->setFontProvider(_fontProvider); - } - } - -private: - std::vector> _textLayers; - - std::shared_ptr _fontProvider; -}; - -} - -#endif /* LayerFontProvider_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerImageProvider.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerImageProvider.cpp deleted file mode 100644 index 536c0437493..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerImageProvider.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "LayerImageProvider.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerImageProvider.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerImageProvider.hpp deleted file mode 100644 index d9affbd1cf1..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerImageProvider.hpp +++ /dev/null @@ -1,58 +0,0 @@ -#ifndef LayerImageProvider_hpp -#define LayerImageProvider_hpp - -#include "Lottie/Public/ImageProvider/AnimationImageProvider.hpp" -#include "Lottie/Private/Model/Assets/ImageAsset.hpp" -#include "Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.hpp" - -namespace lottie { - -/// Connects a LottieImageProvider to a group of image layers -class LayerImageProvider { -public: - LayerImageProvider(std::shared_ptr const &imageProvider, std::map> const &assets) : - _imageProvider(imageProvider), - _imageAssets(assets) { - reloadImages(); - } - - std::shared_ptr imageProvider() const { - return _imageProvider; - } - void setImageProvider(std::shared_ptr const &imageProvider) { - _imageProvider = imageProvider; - reloadImages(); - } - - std::vector> const &imageLayers() const { - return _imageLayers; - } - - void addImageLayers(std::vector> const &layers) { - for (const auto &layer : layers) { - auto it = _imageAssets.find(layer->imageReferenceID()); - if (it != _imageAssets.end()) { - _imageLayers.push_back(layer); - } - } - } - - void reloadImages() { - for (const auto &imageLayer : imageLayers()) { - auto it = _imageAssets.find(imageLayer->imageReferenceID()); - if (it != _imageAssets.end()) { - imageLayer->setImage(_imageProvider->imageForAsset(*it->second)); - } - } - } - -private: - std::shared_ptr _imageProvider; - std::vector> _imageLayers; - - std::map> _imageAssets; -}; - -} - -#endif /* LayerImageProvider_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerTextProvider.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerTextProvider.cpp deleted file mode 100644 index 22da907bd36..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerTextProvider.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "LayerTextProvider.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerTextProvider.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerTextProvider.hpp deleted file mode 100644 index 82c96ec8f39..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerTextProvider.hpp +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef LayerTextProvider_hpp -#define LayerTextProvider_hpp - -#include "Lottie/Public/TextProvider/AnimationTextProvider.hpp" -#include "Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp" - -namespace lottie { - -/// Connects a LottieTextProvider to a group of text layers -class LayerTextProvider { -public: - LayerTextProvider(std::shared_ptr const &textProvider) { - _textProvider = textProvider; - reloadTexts(); - } - - std::shared_ptr const &textProvider() const { - return _textProvider; - } - void setTextProvider(std::shared_ptr const &textProvider) { - _textProvider = textProvider; - reloadTexts(); - } - - void addTextLayers(std::vector> const &layers) { - for (const auto &layer : layers) { - _textLayers.push_back(layer); - } - } - - void reloadTexts() { - for (const auto &layer : _textLayers) { - layer->setTextProvider(_textProvider); - } - } - -private: - std::vector> _textLayers; - - std::shared_ptr _textProvider; -}; - -} - -#endif /* LayerTextProvider_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerTransformNode.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerTransformNode.cpp deleted file mode 100644 index 8f751a571f3..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerTransformNode.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "LayerTransformNode.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerTransformNode.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerTransformNode.hpp deleted file mode 100644 index 459558a2bd3..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/Utility/LayerTransformNode.hpp +++ /dev/null @@ -1,205 +0,0 @@ -#ifndef LayerTransformNode_hpp -#define LayerTransformNode_hpp - -#include "Lottie/Private/Model/Objects/Transform.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PassThroughOutputNode.hpp" - -namespace lottie { - -class LayerTransformProperties: public KeypathSearchableNodePropertyMap { -public: - LayerTransformProperties(std::shared_ptr transform) { - _anchor = std::make_shared>(std::make_shared>(transform->anchorPoint().keyframes)); - _scale = std::make_shared>(std::make_shared>(transform->scale().keyframes)); - _rotation = std::make_shared>(std::make_shared>(transform->rotation().keyframes)); - _opacity = std::make_shared>(std::make_shared>(transform->opacity().keyframes)); - - std::map> propertyMap; - _keypathProperties.insert(std::make_pair("Anchor Point", _anchor)); - _keypathProperties.insert(std::make_pair("Scale", _scale)); - _keypathProperties.insert(std::make_pair("Rotation", _rotation)); - _keypathProperties.insert(std::make_pair("Opacity", _opacity)); - - if (transform->positionX().has_value() && transform->positionY().has_value()) { - auto xPosition = std::make_shared>(std::make_shared>(transform->positionX()->keyframes)); - auto yPosition = std::make_shared>(std::make_shared>(transform->positionY()->keyframes)); - _keypathProperties.insert(std::make_pair("X Position", xPosition)); - _keypathProperties.insert(std::make_pair("Y Position", yPosition)); - - _positionX = xPosition; - _positionY = yPosition; - _position = nullptr; - } else if (transform->position().has_value()) { - auto position = std::make_shared>(std::make_shared>(transform->position()->keyframes)); - _keypathProperties.insert(std::make_pair("Position", position)); - - _position = position; - _positionX = nullptr; - _positionY = nullptr; - } else { - _position = nullptr; - _positionX = nullptr; - _positionY = nullptr; - } - - for (const auto &it : _keypathProperties) { - _properties.push_back(it.second); - } - } - - virtual ~LayerTransformProperties() = default; - - virtual std::vector> &properties() override { - return _properties; - } - - virtual std::vector> const &childKeypaths() const override { - return _childKeypaths; - } - - virtual std::string keypathName() const override { - return "Transform"; - } - - virtual std::map> keypathProperties() const override { - return _keypathProperties; - } - - virtual std::shared_ptr keypathLayer() const override { - return nullptr; - } - - std::shared_ptr> const &anchor() { - return _anchor; - } - - std::shared_ptr> const &scale() { - return _scale; - } - - std::shared_ptr> const &rotation() { - return _rotation; - } - - std::shared_ptr> const &position() { - return _position; - } - - std::shared_ptr> const &positionX() { - return _positionX; - } - - std::shared_ptr> const &positionY() { - return _positionY; - } - - std::shared_ptr> const &opacity() { - return _opacity; - } - -private: - std::map> _keypathProperties; - std::vector> _childKeypaths; - - std::vector> _properties; - - std::shared_ptr> _anchor; - std::shared_ptr> _scale; - std::shared_ptr> _rotation; - std::shared_ptr> _position; - std::shared_ptr> _positionX; - std::shared_ptr> _positionY; - std::shared_ptr> _opacity; -}; - -class LayerTransformNode: public AnimatorNode { -public: - LayerTransformNode(std::shared_ptr transform) : - AnimatorNode(nullptr), - _transformProperties(std::make_shared(transform)) { - _outputNode = std::make_shared(nullptr); - } - - virtual ~LayerTransformNode() = default; - - virtual std::shared_ptr outputNode() override { - return _outputNode; - } - - virtual std::shared_ptr propertyMap() const override { - return _transformProperties; - } - - virtual bool shouldRebuildOutputs(float frame) override { - return hasLocalUpdates() || hasUpstreamUpdates(); - } - - virtual void rebuildOutputs(float frame) override { - _opacity = ((float)_transformProperties->opacity()->value().value) * 0.01f; - - Vector2D position(0.0, 0.0); - if (_transformProperties->position()) { - auto position3d = _transformProperties->position()->value(); - position.x = position3d.x; - position.y = position3d.y; - } else if (_transformProperties->positionX() && _transformProperties->positionY()) { - position = Vector2D( - _transformProperties->positionX()->value().value, - _transformProperties->positionY()->value().value - ); - } - - Vector3D anchor = _transformProperties->anchor()->value(); - Vector3D scale = _transformProperties->scale()->value(); - _localTransform = Transform2D::makeTransform( - Vector2D(anchor.x, anchor.y), - position, - Vector2D(scale.x, scale.y), - _transformProperties->rotation()->value().value, - std::nullopt, - std::nullopt - ); - - if (parentNode() && parentNode()->asLayerTransformNode()) { - _globalTransform = _localTransform * parentNode()->asLayerTransformNode()->_globalTransform; - } else { - _globalTransform = _localTransform; - } - } - - std::shared_ptr const &transformProperties() { - return _transformProperties; - } - - float opacity() { - return _opacity; - } - - Transform2D const &globalTransform() { - return _globalTransform; - } - -private: - std::shared_ptr _outputNode; - - std::shared_ptr _transformProperties; - - float _opacity = 1.0; - Transform2D _localTransform = Transform2D::identity(); - Transform2D _globalTransform = Transform2D::identity(); - -public: - virtual LayerTransformNode *asLayerTransformNode() override { - return this; - } -}; - -} - -#endif /* LayerTransformNode_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.cpp deleted file mode 100644 index eb9e3a58374..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "NodeProperty.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp deleted file mode 100644 index f77cdc470ba..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef NodeProperty_hpp -#define NodeProperty_hpp - -#include "Lottie/Public/Primitives/AnyValue.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.hpp" -#include "Lottie/Public/DynamicProperties/AnyValueProvider.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.hpp" - -namespace lottie { - -/// A node property that holds a reference to a T ValueProvider and a T ValueContainer. -template -class NodeProperty: public AnyNodeProperty { -public: - NodeProperty(std::shared_ptr> provider) : - _typedContainer(provider->value(0.0)), - _valueProvider(provider) { - _typedContainer.setNeedsUpdate(); - } - -public: - virtual AnyValue::Type valueType() const override { - return AnyValueType::type(); - } - - virtual T value() { - return _typedContainer.outputValue(); - } - - virtual bool needsUpdate(float frame) const override { - return _typedContainer.needsUpdate() || _valueProvider->hasUpdate(frame); - } - - virtual void setProvider(std::shared_ptr provider) override { - /*if (provider->valueType() != valueType()) { - return; - } - _valueProvider = provider; - _typedContainer.setNeedsUpdate();*/ - } - - virtual void update(float frame) override { - _typedContainer.setValue(_valueProvider->value(frame), frame); - } - -private: - ValueContainer _typedContainer; - std::shared_ptr> _valueProvider; - //std::shared_ptr _originalValueProvider; -}; - -} - -#endif /* NodeProperty_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.cpp deleted file mode 100644 index 8609641d496..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "AnyNodeProperty.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.hpp deleted file mode 100644 index ec682ceb932..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef AnyNodeProperty_hpp -#define AnyNodeProperty_hpp - -#include "Lottie/Public/Primitives/AnyValue.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.hpp" -#include "Lottie/Public/DynamicProperties/AnyValueProvider.hpp" - -#include - -namespace lottie { - -/// A property of a node. The node property holds a provider and a container -class AnyNodeProperty { -public: - virtual ~AnyNodeProperty() = default; - -public: - /// Returns true if the property needs to recompute its stored value - virtual bool needsUpdate(float frame) const = 0; - - /// Updates the property for the frame - virtual void update(float frame) = 0; - - /// The Type of the value provider - virtual AnyValue::Type valueType() const = 0; - - /// Sets the value provider for the property. - virtual void setProvider(std::shared_ptr provider) = 0; -}; - -} - -#endif /* AnyNodeProperty_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.cpp deleted file mode 100644 index b186f2e2f49..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "AnyValueContainer.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.hpp deleted file mode 100644 index 1776bd13bc1..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef AnyValueContainer_hpp -#define AnyValueContainer_hpp - -#include "Lottie/Public/Primitives/AnyValue.hpp" - -namespace lottie { - -class AnyValueContainer { -public: - /// The stored value of the container - virtual AnyValue value() const = 0; - - /// Notifies the provider that it should update its container - virtual void setNeedsUpdate() = 0; - - /// When true the container needs to have its value updated by its provider - virtual bool needsUpdate() const = 0; - - /// The frame time of the last provided update - virtual float lastUpdateFrame() const = 0; -}; - -} - -#endif /* AnyValueContainer_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasRenderUpdates.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasRenderUpdates.hpp deleted file mode 100644 index bc6a13261e2..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasRenderUpdates.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef HasRenderUpdates_hpp -#define HasRenderUpdates_hpp - -namespace lottie { - -class HasRenderUpdates { -public: - virtual bool hasRenderUpdates(float forFrame) = 0; -}; - -} - -#endif /* HasRenderUpdates_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasUpdate.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasUpdate.hpp deleted file mode 100644 index f0c35b05308..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasUpdate.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef HasUpdate_hpp -#define HasUpdate_hpp - -namespace lottie { - -class HasUpdate { -public: - /// The last frame in which this node was updated. - virtual bool hasUpdate() = 0; -}; - -} - -#endif /* HasUpdate_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.cpp deleted file mode 100644 index 62c08facbad..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "KeypathSearchable.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp deleted file mode 100644 index 4586c7ddc24..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef KeypathSearchable_hpp -#define KeypathSearchable_hpp - -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.hpp" -#include "Lottie/Public/Primitives/CALayer.hpp" - -#include -#include -#include -#include - -namespace lottie { - -class KeypathSearchable; - -class HasChildKeypaths { -public: - /// Children Keypaths - virtual std::vector> const &childKeypaths() const = 0; -}; - -/// Protocol that provides keypath search functionality. Returns all node properties associated with a keypath. -class KeypathSearchable: virtual public HasChildKeypaths { -public: - /// The name of the Keypath - virtual std::string keypathName() const = 0; - - /// A list of properties belonging to the keypath. - virtual std::map> keypathProperties() const = 0; - - virtual std::shared_ptr keypathLayer() const = 0; -}; - -} - -#endif /* KeypathSearchable_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.cpp deleted file mode 100644 index 100edba8df9..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "NodePropertyMap.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp deleted file mode 100644 index 0879d00c828..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef NodePropertyMap_hpp -#define NodePropertyMap_hpp - -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp" -#include "Lottie/Public/Primitives/CALayer.hpp" - -#include - -namespace lottie { - -class NodePropertyMap: virtual public HasChildKeypaths { -public: - virtual std::vector> &properties() = 0; - - bool needsLocalUpdate(float frame) { - for (auto &property : properties()) { - if (property->needsUpdate(frame)) { - return true; - } - } - return false; - } - - void updateNodeProperties(float frame) { - for (auto &property : properties()) { - property->update(frame); - } - } -}; - -class KeypathSearchableNodePropertyMap: virtual public NodePropertyMap, virtual public KeypathSearchable { -public: - virtual std::shared_ptr keypathLayer() const override { - return nullptr; - } -}; - -} - -#endif /* NodePropertyMap_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.cpp deleted file mode 100644 index 2ead9de1edc..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "ValueContainer.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.hpp deleted file mode 100644 index 563de16de5b..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.hpp +++ /dev/null @@ -1,58 +0,0 @@ -#ifndef ValueContainer_hpp -#define ValueContainer_hpp - -#include "Lottie/Public/Primitives/AnyValue.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.hpp" - -namespace lottie { - -/// A container for a node value that is Typed to T. -template -class ValueContainer: public AnyValueContainer { -public: - ValueContainer(T value) : - _outputValue(value) { - } - -public: - float _lastUpdateFrame = std::numeric_limits::infinity(); - bool _needsUpdate = true; - - virtual AnyValue value() const override { - return AnyValue(_outputValue); - } - - virtual bool needsUpdate() const override { - return _needsUpdate; - } - - virtual float lastUpdateFrame() const override { - return _lastUpdateFrame; - } - - T _outputValue; - - T outputValue() { - return _outputValue; - } - void setOutputValue(T value) { - _outputValue = value; - _needsUpdate = false; - } - - void setValue(AnyValue value, float forFrame) { - if (value.type() == AnyValueType::type()) { - _needsUpdate = false; - _lastUpdateFrame = forFrame; - _outputValue = value.get(); - } - } - - virtual void setNeedsUpdate() override { - _needsUpdate = true; - } -}; - -} - -#endif /* ValueContainer_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.cpp deleted file mode 100644 index b3f91f556b3..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "DashPatternInterpolator.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.hpp deleted file mode 100644 index 5e744386dd5..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.hpp +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef DashPatternInterpolator_hpp -#define DashPatternInterpolator_hpp - -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp" -#include "Lottie/Public/Primitives/DashPattern.hpp" - -namespace lottie { - -/// A value provider that produces an array of values from an array of Keyframe Interpolators -class DashPatternInterpolator: public ValueProvider, public std::enable_shared_from_this { -public: - /// Initialize with an array of array of keyframes. - DashPatternInterpolator(std::vector>> const &keyframeGroups) { - for (const auto &keyframeGroup : keyframeGroups) { - _keyframeInterpolators.push_back(std::make_shared>(keyframeGroup)); - } - } - - virtual ~DashPatternInterpolator() = default; - - virtual AnyValue::Type valueType() const override { - return AnyValueType::type(); - } - - virtual DashPattern value(AnimationFrameTime frame) override { - std::vector values; - for (const auto &interpolator : _keyframeInterpolators) { - values.push_back(interpolator->value(frame).value); - } - return DashPattern(std::move(values)); - } - - virtual bool hasUpdate(float frame) const override { - for (const auto &interpolator : _keyframeInterpolators) { - if (interpolator->hasUpdate(frame)) { - return true; - } - } - return false; - } - -private: - std::vector>> _keyframeInterpolators; -}; - -} - -#endif /* DashPatternInterpolator_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.cpp deleted file mode 100644 index 8ec23647627..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "KeyframeInterpolator.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp deleted file mode 100644 index de35d4887aa..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp +++ /dev/null @@ -1,452 +0,0 @@ -#ifndef KeyframeInterpolator_hpp -#define KeyframeInterpolator_hpp - -#include "Lottie/Public/DynamicProperties/AnyValueProvider.hpp" - -namespace lottie { - -/// A value provider that produces a value at Time from a group of keyframes -template -class KeyframeInterpolator: public ValueProvider, public std::enable_shared_from_this> { -public: - KeyframeInterpolator(std::vector> const &keyframes_) : - keyframes(keyframes_) { - assert(!keyframes.empty()); - } - - virtual ~KeyframeInterpolator() { - } - -public: - std::vector> keyframes; - - virtual AnyValue::Type valueType() const override { - return AnyValueType::type(); - } - - virtual T value(AnimationFrameTime frame) override { - // First set the keyframe span for the frame. - updateSpanIndices(frame); - lastUpdatedFrame = frame; - // If only one keyframe return its value - - if (leadingKeyframe.has_value() && - trailingKeyframe.has_value()) - { - /// We have leading and trailing keyframe. - auto progress = leadingKeyframe->interpolatedProgress(trailingKeyframe.value(), frame); - return leadingKeyframe->interpolate(trailingKeyframe.value(), progress); - } else if (leadingKeyframe.has_value()) { - return leadingKeyframe->value; - } else if (trailingKeyframe.has_value()) { - return trailingKeyframe->value; - } else { - /// Satisfy the compiler. - return keyframes[0].value; - } - } - - /// Returns true to trigger a frame update for this interpolator. - /// - /// An interpolator will be asked if it needs to update every frame. - /// If the interpolator needs updating it will be asked to compute its value for - /// the given frame. - /// - /// Cases a keyframe should not be updated: - /// - If time is in span and leading keyframe is hold - /// - If time is after the last keyframe. - /// - If time is before the first keyframe - /// - /// Cases for updating a keyframe: - /// - If time is in the span, and is not a hold - /// - If time is outside of the span, and there are more keyframes - /// - If a value delegate is set - /// - If leading and trailing are both nil. - virtual bool hasUpdate(float frame) const override { - if (!lastUpdatedFrame.has_value()) { - return true; - } - - if (leadingKeyframe.has_value() && - !trailingKeyframe.has_value() && - leadingKeyframe->time < frame) - { - /// Frame is after bounds of keyframes - return false; - } - if (trailingKeyframe.has_value() && - !leadingKeyframe.has_value() && - frame < trailingKeyframe->time) - { - /// Frame is before bounds of keyframes - return false; - } - if (leadingKeyframe.has_value() && - trailingKeyframe.has_value() && - leadingKeyframe->isHold && - leadingKeyframe->time < frame && - frame < trailingKeyframe->time) - { - return false; - } - return true; - } - - // MARK: Fileprivate - - std::optional lastUpdatedFrame; - - std::optional leadingIndex; - std::optional trailingIndex; - std::optional> leadingKeyframe; - std::optional> trailingKeyframe; - - /// Finds the appropriate Leading and Trailing keyframe index for the given time. - void updateSpanIndices(float frame) { - if (keyframes.empty()) { - leadingIndex = std::nullopt; - trailingIndex = std::nullopt; - leadingKeyframe = std::nullopt; - trailingKeyframe = std::nullopt; - return; - } - - // This function searches through the array to find the span of two keyframes - // that contain the current time. - // - // We could use Array.first(where:) but that would search through the entire array - // each frame. - // Instead we track the last used index and search either forwards or - // backwards from there. This reduces the iterations and complexity from - // - // O(n), where n is the length of the sequence to - // O(n), where n is the number of items after or before the last used index. - // - - if (keyframes.size() == 1) { - /// Only one keyframe. Set it as first and move on. - leadingIndex = 0; - trailingIndex = std::nullopt; - leadingKeyframe = keyframes[0]; - trailingKeyframe = std::nullopt; - return; - } - - /// Sets the initial keyframes. This is often only needed for the first check. - if - (!leadingIndex.has_value() && - !trailingIndex.has_value()) - { - if (frame < keyframes[0].time) { - /// Time is before the first keyframe. Set it as the trailing. - trailingIndex = 0; - } else { - /// Time is after the first keyframe. Set the keyframe and the trailing. - leadingIndex = 0; - trailingIndex = 1; - } - } - - if - (trailingIndex.has_value() && - keyframes[trailingIndex.value()].time <= frame) - { - /// Time is after the current span. Iterate forward. - auto newLeading = trailingIndex.value(); - bool keyframeFound = false; - while (!keyframeFound) { - leadingIndex = newLeading; - if (newLeading + 1 >= 0 && newLeading + 1 < keyframes.size()) { - trailingIndex = newLeading + 1; - } else { - trailingIndex = std::nullopt; - } - - if (!trailingIndex.has_value()) { - /// We have reached the end of our keyframes. Time is after the last keyframe. - keyframeFound = true; - continue; - } - - if (frame < keyframes[trailingIndex.value()].time) { - /// Keyframe in current span. - keyframeFound = true; - continue; - } - /// Advance the array. - newLeading = trailingIndex.value(); - } - - } else if - (leadingIndex.has_value() && - frame < keyframes[leadingIndex.value()].time) - { - - /// Time is before the current span. Iterate backwards - auto newTrailing = leadingIndex.value(); - - bool keyframeFound = false; - while (!keyframeFound) { - if (newTrailing - 1 >= 0 && newTrailing - 1 < keyframes.size()) { - leadingIndex = newTrailing - 1; - } else { - leadingIndex = std::nullopt; - } - trailingIndex = newTrailing; - - if (!leadingIndex.has_value()) { - /// We have reached the end of our keyframes. Time is after the last keyframe. - keyframeFound = true; - continue; - } - if (keyframes[leadingIndex.value()].time <= frame) { - /// Keyframe in current span. - keyframeFound = true; - continue; - } - /// Step back - newTrailing = leadingIndex.value(); - } - } - if (const auto keyFrame = leadingIndex) { - leadingKeyframe = keyframes[keyFrame.value()]; - } else { - leadingKeyframe = std::nullopt; - } - - if (const auto keyFrame = trailingIndex) { - trailingKeyframe = keyframes[keyFrame.value()]; - } else { - trailingKeyframe = std::nullopt; - } - } -}; - -class BezierPathKeyframeInterpolator { -public: - BezierPathKeyframeInterpolator(std::vector> const &keyframes_) : - keyframes(keyframes_) { - assert(!keyframes.empty()); - } - -public: - std::vector> keyframes; - - void update(AnimationFrameTime frame, BezierPath &outPath) { - // First set the keyframe span for the frame. - updateSpanIndices(frame); - lastUpdatedFrame = frame; - // If only one keyframe return its value - - if (leadingKeyframe.has_value() && - trailingKeyframe.has_value()) - { - /// We have leading and trailing keyframe. - auto progress = leadingKeyframe->interpolatedProgress(trailingKeyframe.value(), frame); - interpolateInplace(leadingKeyframe.value(), trailingKeyframe.value(), progress, outPath); - } else if (leadingKeyframe.has_value()) { - setInplace(leadingKeyframe.value(), outPath); - } else if (trailingKeyframe.has_value()) { - setInplace(trailingKeyframe.value(), outPath); - } else { - /// Satisfy the compiler. - setInplace(keyframes[0], outPath); - } - } - - /// Returns true to trigger a frame update for this interpolator. - /// - /// An interpolator will be asked if it needs to update every frame. - /// If the interpolator needs updating it will be asked to compute its value for - /// the given frame. - /// - /// Cases a keyframe should not be updated: - /// - If time is in span and leading keyframe is hold - /// - If time is after the last keyframe. - /// - If time is before the first keyframe - /// - /// Cases for updating a keyframe: - /// - If time is in the span, and is not a hold - /// - If time is outside of the span, and there are more keyframes - /// - If a value delegate is set - /// - If leading and trailing are both nil. - bool hasUpdate(float frame) const { - if (!lastUpdatedFrame.has_value()) { - return true; - } - - if (leadingKeyframe.has_value() && - !trailingKeyframe.has_value() && - leadingKeyframe->time < frame) - { - /// Frame is after bounds of keyframes - return false; - } - if (trailingKeyframe.has_value() && - !leadingKeyframe.has_value() && - frame < trailingKeyframe->time) - { - /// Frame is before bounds of keyframes - return false; - } - if (leadingKeyframe.has_value() && - trailingKeyframe.has_value() && - leadingKeyframe->isHold && - leadingKeyframe->time < frame && - frame < trailingKeyframe->time) - { - return false; - } - return true; - } - - // MARK: Fileprivate - - std::optional lastUpdatedFrame; - - std::optional leadingIndex; - std::optional trailingIndex; - std::optional> leadingKeyframe; - std::optional> trailingKeyframe; - - /// Finds the appropriate Leading and Trailing keyframe index for the given time. - void updateSpanIndices(float frame) { - if (keyframes.empty()) { - leadingIndex = std::nullopt; - trailingIndex = std::nullopt; - leadingKeyframe = std::nullopt; - trailingKeyframe = std::nullopt; - return; - } - - // This function searches through the array to find the span of two keyframes - // that contain the current time. - // - // We could use Array.first(where:) but that would search through the entire array - // each frame. - // Instead we track the last used index and search either forwards or - // backwards from there. This reduces the iterations and complexity from - // - // O(n), where n is the length of the sequence to - // O(n), where n is the number of items after or before the last used index. - // - - if (keyframes.size() == 1) { - /// Only one keyframe. Set it as first and move on. - leadingIndex = 0; - trailingIndex = std::nullopt; - leadingKeyframe = keyframes[0]; - trailingKeyframe = std::nullopt; - return; - } - - /// Sets the initial keyframes. This is often only needed for the first check. - if - (!leadingIndex.has_value() && - !trailingIndex.has_value()) - { - if (frame < keyframes[0].time) { - /// Time is before the first keyframe. Set it as the trailing. - trailingIndex = 0; - } else { - /// Time is after the first keyframe. Set the keyframe and the trailing. - leadingIndex = 0; - trailingIndex = 1; - } - } - - if - (trailingIndex.has_value() && - keyframes[trailingIndex.value()].time <= frame) - { - /// Time is after the current span. Iterate forward. - auto newLeading = trailingIndex.value(); - bool keyframeFound = false; - while (!keyframeFound) { - leadingIndex = newLeading; - if (newLeading + 1 >= 0 && newLeading + 1 < keyframes.size()) { - trailingIndex = newLeading + 1; - } else { - trailingIndex = std::nullopt; - } - - if (!trailingIndex.has_value()) { - /// We have reached the end of our keyframes. Time is after the last keyframe. - keyframeFound = true; - continue; - } - - if (frame < keyframes[trailingIndex.value()].time) { - /// Keyframe in current span. - keyframeFound = true; - continue; - } - /// Advance the array. - newLeading = trailingIndex.value(); - } - - } else if - (leadingIndex.has_value() && - frame < keyframes[leadingIndex.value()].time) - { - - /// Time is before the current span. Iterate backwards - auto newTrailing = leadingIndex.value(); - - bool keyframeFound = false; - while (!keyframeFound) { - if (newTrailing - 1 >= 0 && newTrailing - 1 < keyframes.size()) { - leadingIndex = newTrailing - 1; - } else { - leadingIndex = std::nullopt; - } - trailingIndex = newTrailing; - - if (!leadingIndex.has_value()) { - /// We have reached the end of our keyframes. Time is after the last keyframe. - keyframeFound = true; - continue; - } - if (keyframes[leadingIndex.value()].time <= frame) { - /// Keyframe in current span. - keyframeFound = true; - continue; - } - /// Step back - newTrailing = leadingIndex.value(); - } - } - if (const auto keyFrame = leadingIndex) { - leadingKeyframe = keyframes[keyFrame.value()]; - } else { - leadingKeyframe = std::nullopt; - } - - if (const auto keyFrame = trailingIndex) { - trailingKeyframe = keyframes[keyFrame.value()]; - } else { - trailingKeyframe = std::nullopt; - } - } - -private: - void setInplace(Keyframe const &from, BezierPath &outPath) { - ValueInterpolator::setInplace(from.value, outPath); - } - - void interpolateInplace(Keyframe const &from, Keyframe const &to, float progress, BezierPath &outPath) { - std::optional spatialOutTangent2d; - if (from.spatialOutTangent) { - spatialOutTangent2d = Vector2D(from.spatialOutTangent->x, from.spatialOutTangent->y); - } - std::optional spatialInTangent2d; - if (to.spatialInTangent) { - spatialInTangent2d = Vector2D(to.spatialInTangent->x, to.spatialInTangent->y); - } - ValueInterpolator::interpolateInplace(from.value, to.value, progress, spatialOutTangent2d, spatialInTangent2d, outPath); - } -}; - -} - -#endif /* KeyframeInterpolator_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.cpp deleted file mode 100644 index face3214996..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "SingleValueProvider.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.hpp deleted file mode 100644 index b6c3fe6a1f8..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.hpp +++ /dev/null @@ -1,42 +0,0 @@ -#ifndef SingleValueProvider_hpp -#define SingleValueProvider_hpp - -#include "Lottie/Public/DynamicProperties/AnyValueProvider.hpp" - -namespace lottie { - -/// Returns a value for every frame. -template -class SingleValueProvider: public ValueProvider { -public: - SingleValueProvider(T const &value) : - _value(value) { - } - - virtual ~SingleValueProvider() = default; - - void setValue(T const &value) { - _value = value; - _hasUpdate = true; - } - - virtual T value(AnimationFrameTime frame) override { - return _value; - } - - virtual AnyValue::Type valueType() const override { - return AnyValueType::type(); - } - - virtual bool hasUpdate(float frame) const override { - return _hasUpdate; - } - -private: - T _value; - bool _hasUpdate = true; -}; - -} - -#endif /* SingleValueProvider_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PassThroughOutputNode.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PassThroughOutputNode.hpp deleted file mode 100644 index f7b73a550df..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PassThroughOutputNode.hpp +++ /dev/null @@ -1,72 +0,0 @@ -#ifndef PassThroughOutputNode_hpp -#define PassThroughOutputNode_hpp - -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasRenderUpdates.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasUpdate.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp" - -namespace lottie { - -class PassThroughOutputNode: virtual public NodeOutput, virtual public HasRenderUpdates, virtual public HasUpdate { -public: - PassThroughOutputNode(std::shared_ptr parent) : - _parent(parent) { - } - - virtual ~PassThroughOutputNode() = default; - - virtual std::shared_ptr parent() override { - return _parent; - } - - virtual bool isEnabled() const override { - return _isEnabled; - } - virtual void setIsEnabled(bool isEnabled) override { - _isEnabled = isEnabled; - } - - virtual bool hasUpdate() override { - return _hasUpdate; - } - void setHasUpdate(bool hasUpdate) { - _hasUpdate = hasUpdate; - } - - virtual std::shared_ptr outputPath() override { - if (_parent) { - return _parent->outputPath(); - } - return nullptr; - } - - virtual bool hasOutputUpdates(float forFrame) override { - /// Changes to this node do not affect downstream nodes. - bool parentUpdate = false; - if (_parent) { - parentUpdate = _parent->hasOutputUpdates(forFrame); - } - /// Changes to upstream nodes do, however, affect this nodes state. - _hasUpdate = _hasUpdate || parentUpdate; - return parentUpdate; - } - - virtual bool hasRenderUpdates(float forFrame) override { - /// Return true if there are upstream updates or if this node has updates - bool upstreamUpdates = false; - if (_parent) { - upstreamUpdates = _parent->hasOutputUpdates(forFrame); - } - _hasUpdate = _hasUpdate || upstreamUpdates; - return _hasUpdate; - } - -private: - std::shared_ptr _parent; - bool _hasUpdate = false; - bool _isEnabled = true; -}; - -} - -#endif /* PassThroughOutputNode_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.hpp deleted file mode 100644 index 731a81149c2..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef StrokeNode_hpp -#define StrokeNode_hpp - -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp" -#include "Lottie/Private/Model/ShapeItems/Stroke.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/SingleValueProvider.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/RenderNode.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/DashPatternInterpolator.hpp" - -namespace lottie { - -class StrokeShapeDashConfiguration { -public: - StrokeShapeDashConfiguration(std::vector const &elements) { - /// Converts the `[DashElement]` data model into `lineDashPattern` and `lineDashPhase` - /// representations usable in a `CAShapeLayer` - for (const auto &dash : elements) { - if (dash.type == DashElementType::Offset) { - dashPhase = dash.value.keyframes; - } else { - dashPatterns.push_back(dash.value.keyframes); - } - } - } - -public: - std::vector>> dashPatterns; - std::vector> dashPhase; -}; - -} - -#endif /* StrokeNode_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.hpp deleted file mode 100644 index 6e8b534b484..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.hpp +++ /dev/null @@ -1,367 +0,0 @@ -#ifndef TextAnimatorNode_hpp -#define TextAnimatorNode_hpp - -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp" -#include "Lottie/Private/Model/Text/TextAnimator.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp" - -namespace lottie { - -class TextAnimatorNodeProperties: public KeypathSearchableNodePropertyMap { -public: - TextAnimatorNodeProperties(std::shared_ptr const &textAnimator) { - _keypathName = textAnimator->name.value_or(""); - - if (textAnimator->anchor) { - _anchor = std::make_shared>(std::make_shared>(textAnimator->anchor->keyframes)); - _keypathProperties.insert(std::make_pair("Anchor", _anchor)); - } - - if (textAnimator->position) { - _position = std::make_shared>(std::make_shared>(textAnimator->position->keyframes)); - _keypathProperties.insert(std::make_pair("Position", _position)); - } - - if (textAnimator->scale) { - _scale = std::make_shared>(std::make_shared>(textAnimator->scale->keyframes)); - _keypathProperties.insert(std::make_pair("Scale", _scale)); - } - - if (textAnimator->skew) { - _skew = std::make_shared>(std::make_shared>(textAnimator->skew->keyframes)); - _keypathProperties.insert(std::make_pair("Skew", _skew)); - } - - if (textAnimator->skewAxis) { - _skewAxis = std::make_shared>(std::make_shared>(textAnimator->skewAxis->keyframes)); - _keypathProperties.insert(std::make_pair("Skew Axis", _skewAxis)); - } - - if (textAnimator->rotation) { - _rotation = std::make_shared>(std::make_shared>(textAnimator->rotation->keyframes)); - _keypathProperties.insert(std::make_pair("Rotation", _rotation)); - } - - if (textAnimator->rotation) { - _opacity = std::make_shared>(std::make_shared>(textAnimator->opacity->keyframes)); - _keypathProperties.insert(std::make_pair("Opacity", _opacity)); - } - - if (textAnimator->strokeColor) { - _strokeColor = std::make_shared>(std::make_shared>(textAnimator->strokeColor->keyframes)); - _keypathProperties.insert(std::make_pair("Stroke Color", _strokeColor)); - } - - if (textAnimator->fillColor) { - _fillColor = std::make_shared>(std::make_shared>(textAnimator->fillColor->keyframes)); - _keypathProperties.insert(std::make_pair("Fill Color", _fillColor)); - } - - if (textAnimator->strokeWidth) { - _strokeWidth = std::make_shared>(std::make_shared>(textAnimator->strokeWidth->keyframes)); - _keypathProperties.insert(std::make_pair("Stroke Width", _strokeWidth)); - } - - if (textAnimator->tracking) { - _tracking = std::make_shared>(std::make_shared>(textAnimator->tracking->keyframes)); - _keypathProperties.insert(std::make_pair("Tracking", _tracking)); - } - - for (const auto &it : _keypathProperties) { - _properties.push_back(it.second); - } - } - - virtual ~TextAnimatorNodeProperties() = default; - - virtual std::string keypathName() const override { - return _keypathName; - } - - virtual std::map> keypathProperties() const override { - return _keypathProperties; - } - - virtual std::vector> &properties() override { - return _properties; - } - - virtual std::vector> const &childKeypaths() const override { - return _childKeypaths; - } - - Transform2D caTransform() { - Vector2D anchor = Vector2D::Zero(); - if (_anchor) { - auto anchor3d = _anchor->value(); - anchor = Vector2D(anchor3d.x, anchor3d.y); - } - - Vector2D position = Vector2D::Zero(); - if (_position) { - auto position3d = _position->value(); - position = Vector2D(position3d.x, position3d.y); - } - - Vector2D scale = Vector2D(100.0, 100.0); - if (_scale) { - auto scale3d = _scale->value(); - scale = Vector2D(scale3d.x, scale3d.y); - } - - float rotation = 0.0; - if (_rotation) { - rotation = _rotation->value().value; - } - - std::optional skew; - if (_skew) { - skew = _skew->value().value; - } - std::optional skewAxis; - if (_skewAxis) { - skewAxis = _skewAxis->value().value; - } - - return Transform2D::makeTransform( - anchor, - position, - scale, - rotation, - skew, - skewAxis - ); - } - - virtual std::shared_ptr keypathLayer() const override { - return nullptr; - } - - float opacity() { - if (_opacity) { - return _opacity->value().value; - } else { - return 100.0; - } - } - - std::optional strokeColor() { - if (_strokeColor) { - return _strokeColor->value(); - } else { - return std::nullopt; - } - } - - std::optional fillColor() { - if (_fillColor) { - return _fillColor->value(); - } else { - return std::nullopt; - } - } - - float tracking() { - if (_tracking) { - return _tracking->value().value; - } else { - return 1.0; - } - } - - float strokeWidth() { - if (_strokeWidth) { - return _strokeWidth->value().value; - } else { - return 0.0; - } - } - -private: - std::string _keypathName; - - std::shared_ptr> _anchor; - std::shared_ptr> _position; - std::shared_ptr> _scale; - std::shared_ptr> _skew; - std::shared_ptr> _skewAxis; - std::shared_ptr> _rotation; - std::shared_ptr> _opacity; - std::shared_ptr> _strokeColor; - std::shared_ptr> _fillColor; - std::shared_ptr> _strokeWidth; - std::shared_ptr> _tracking; - - std::map> _keypathProperties; - std::vector> _childKeypaths; - std::vector> _properties; -}; - -class TextOutputNode: virtual public NodeOutput { -public: - TextOutputNode(std::shared_ptr parent) : - _parentTextNode(parent) { - } - - virtual ~TextOutputNode() = default; - - virtual std::shared_ptr parent() override { - return _parentTextNode; - } - - Transform2D xform() { - if (_xform.has_value()) { - return _xform.value(); - } else if (_parentTextNode) { - return _parentTextNode->xform(); - } else { - return Transform2D::identity(); - } - } - void setXform(Transform2D const &xform) { - _xform = xform; - } - - float opacity() { - if (_opacity.has_value()) { - return _opacity.value(); - } else if (_parentTextNode) { - return _parentTextNode->opacity(); - } else { - return 1.0; - } - } - void setOpacity(float opacity) { - _opacity = opacity; - } - - std::optional strokeColor() { - if (_strokeColor.has_value()) { - return _strokeColor.value(); - } else if (_parentTextNode) { - return _parentTextNode->strokeColor(); - } else { - return std::nullopt; - } - } - void setStrokeColor(std::optional strokeColor) { - _strokeColor = strokeColor; - } - - std::optional fillColor() { - if (_fillColor.has_value()) { - return _fillColor.value(); - } else if (_parentTextNode) { - return _parentTextNode->fillColor(); - } else { - return std::nullopt; - } - } - void setFillColor(std::optional fillColor) { - _fillColor = fillColor; - } - - float tracking() { - if (_tracking.has_value()) { - return _tracking.value(); - } else if (_parentTextNode) { - return _parentTextNode->tracking(); - } else { - return 0.0; - } - } - void setTracking(float tracking) { - _tracking = tracking; - } - - float strokeWidth() { - if (_strokeWidth.has_value()) { - return _strokeWidth.value(); - } else if (_parentTextNode) { - return _parentTextNode->strokeWidth(); - } else { - return 0.0; - } - } - void setStrokeWidth(float strokeWidth) { - _strokeWidth = strokeWidth; - } - - virtual bool hasOutputUpdates(float frame) override { - // TODO Fix This - return true; - } - - virtual std::shared_ptr outputPath() override { - return _outputPath; - } - - virtual bool isEnabled() const override { - return _isEnabled; - } - virtual void setIsEnabled(bool isEnabled) override { - _isEnabled = isEnabled; - } - -private: - std::shared_ptr _parentTextNode; - bool _isEnabled = true; - - std::shared_ptr _outputPath; - - std::optional _xform; - std::optional _opacity; - std::optional _strokeColor; - std::optional _fillColor; - std::optional _tracking; - std::optional _strokeWidth; -}; - -class TextAnimatorNode: public AnimatorNode { -public: - TextAnimatorNode(std::shared_ptr const &parentNode, std::shared_ptr const &textAnimator) : - AnimatorNode(parentNode) { - std::shared_ptr parentOutputNode; - if (parentNode) { - parentOutputNode = parentNode->_textOutputNode; - } - _textOutputNode = std::make_shared(parentOutputNode); - - _textAnimatorProperties = std::make_shared(textAnimator); - } - - virtual ~TextAnimatorNode() = default; - - virtual std::shared_ptr outputNode() override { - return _textOutputNode; - } - - virtual std::shared_ptr propertyMap() const override { - return _textAnimatorProperties; - } - - virtual bool localUpdatesPermeateDownstream() override { - return true; - } - - virtual void rebuildOutputs(float frame) override { - _textOutputNode->setXform(_textAnimatorProperties->caTransform()); - _textOutputNode->setOpacity(((float)_textAnimatorProperties->opacity()) * 0.01f); - _textOutputNode->setStrokeColor(_textAnimatorProperties->strokeColor()); - _textOutputNode->setFillColor(_textAnimatorProperties->fillColor()); - _textOutputNode->setTracking(_textAnimatorProperties->tracking()); - _textOutputNode->setStrokeWidth(_textAnimatorProperties->strokeWidth()); - } - -private: - std::shared_ptr _textOutputNode; - - std::shared_ptr _textAnimatorProperties; -}; - -} - -#endif /* TextAnimatorNode_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp deleted file mode 100644 index 249c0556656..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp +++ /dev/null @@ -1,238 +0,0 @@ -#ifndef AnimatorNode_hpp -#define AnimatorNode_hpp - -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp" - -#include -#include - -namespace lottie { - -class LayerTransformNode; -class PathNode; -class RenderNode; - -/// The Animator Node is the base node in the render system tree. -/// -/// It defines a single node that has an output path and option input node. -/// At animation time the root animation node is asked to update its contents for -/// the current frame. -/// The node reaches up its chain of nodes until the first node that does not need -/// updating is found. Then each node updates its contents down the render pipeline. -/// Each node adds its local path to its input path and passes it forward. -/// -/// An animator node holds a group of interpolators. These interpolators determine -/// if the node needs an update for the current frame. -/// -class AnimatorNode: public KeypathSearchable { -public: - AnimatorNode(std::shared_ptr const &parentNode) : - _parentNode(parentNode) { - } - - AnimatorNode(const AnimatorNode&) = delete; - AnimatorNode& operator=(AnimatorNode&) = delete; - - /// The available properties of the Node. - /// - /// These properties are automatically updated each frame. - /// These properties are also settable and gettable through the dynamic - /// property system. - /// - virtual std::shared_ptr propertyMap() const = 0; - - /// The upstream input node - std::shared_ptr parentNode() { - return _parentNode; - } - void setParentNode(std::shared_ptr const &parentNode) { - _parentNode = parentNode; - } - - /// The output of the node. - virtual std::shared_ptr outputNode() = 0; - - /// Update the outputs of the node. Called if local contents were update or if outputsNeedUpdate returns true. - virtual void rebuildOutputs(float frame) = 0; - - /// Setters for marking current node state. - bool isEnabled() { - return _isEnabled; - } - virtual void setIsEnabled(bool isEnabled) { - _isEnabled = isEnabled; - } - - bool hasLocalUpdates() { - return _hasLocalUpdates; - } - virtual void setHasLocalUpdates(bool hasLocalUpdates) { - _hasLocalUpdates = hasLocalUpdates; - } - - bool hasUpstreamUpdates() { - return _hasUpstreamUpdates; - } - virtual void setHasUpstreamUpdates(bool hasUpstreamUpdates) { - _hasUpstreamUpdates = hasUpstreamUpdates; - } - - std::optional lastUpdateFrame() { - return _lastUpdateFrame; - } - virtual void setLastUpdateFrame(std::optional lastUpdateFrame) { - _lastUpdateFrame = lastUpdateFrame; - } - - /// Marks if updates to this node affect nodes downstream. - virtual bool localUpdatesPermeateDownstream() { - /// Optional override - return true; - } - virtual bool forceUpstreamOutputUpdates() { - /// Optional - return false; - } - - /// Called at the end of this nodes update cycle. Always called. Optional. - virtual bool performAdditionalLocalUpdates(float frame, bool forceLocalUpdate) { - /// Optional - return forceLocalUpdate; - } - virtual void performAdditionalOutputUpdates(float frame, bool forceOutputUpdate) { - /// Optional - } - - /// The default simply returns `hasLocalUpdates` - virtual bool shouldRebuildOutputs(float frame) { - return hasLocalUpdates(); - } - - virtual bool updateOutputs(float frame, bool forceOutputUpdate) { - if (!isEnabled()) { - setLastUpdateFrame(frame); - if (const auto parentNodeValue = parentNode()) { - return parentNodeValue->updateOutputs(frame, forceOutputUpdate); - } else { - return false; - } - } - - if (!forceOutputUpdate && lastUpdateFrame().has_value() && lastUpdateFrame().value() == frame) { - /// This node has already updated for this frame. Go ahead and return the results. - return hasUpstreamUpdates() || hasLocalUpdates(); - } - - /// Ask if this node should force output updates upstream. - bool forceUpstreamUpdates = forceOutputUpdate || forceUpstreamOutputUpdates(); - - /// Perform upstream output updates. Optionally mark upstream updates if any. - if (const auto parentNodeValue = parentNode()) { - setHasUpstreamUpdates(parentNodeValue->updateOutputs(frame, forceUpstreamUpdates) || hasUpstreamUpdates()); - } else { - setHasUpstreamUpdates(hasUpstreamUpdates()); - } - - /// Perform additional local output updates - performAdditionalOutputUpdates(frame, forceUpstreamUpdates); - - /// If there are local updates, or if updates have been force, rebuild outputs - if (forceUpstreamUpdates || shouldRebuildOutputs(frame)) { - setLastUpdateFrame(frame); - rebuildOutputs(frame); - } - return hasUpstreamUpdates() || hasLocalUpdates(); - } - - /// Rebuilds the content of this node, and upstream nodes if necessary. - virtual bool updateContents(float frame, bool forceLocalUpdate) { - if (!isEnabled()) { - // Disabled node, pass through. - if (const auto parentNodeValue = parentNode()) { - return parentNodeValue->updateContents(frame, forceLocalUpdate); - } else { - return false; - } - } - - if (forceLocalUpdate == false && lastUpdateFrame().has_value() && lastUpdateFrame().value() == frame) { - /// This node has already updated for this frame. Go ahead and return the results. - return localUpdatesPermeateDownstream() ? hasUpstreamUpdates() || hasLocalUpdates() : hasUpstreamUpdates(); - } - - /// Are there local updates? If so mark the node. - setHasLocalUpdates(forceLocalUpdate ? forceLocalUpdate : propertyMap()->needsLocalUpdate(frame)); - - /// Were there upstream updates? If so mark the node - if (const auto parentNodeValue = parentNode()) { - setHasUpstreamUpdates(parentNodeValue->updateContents(frame, forceLocalUpdate)); - } else { - setHasUpstreamUpdates(false); - } - - /// Perform property updates if necessary. - if (hasLocalUpdates()) { - /// Rebuild local properties - propertyMap()->updateNodeProperties(frame); - } - - /// Ask the node to perform any other updates it might have. - setHasUpstreamUpdates(performAdditionalLocalUpdates(frame, forceLocalUpdate) || hasUpstreamUpdates()); - - /// If the node can update nodes downstream, notify them, otherwise pass on any upstream updates downstream. - return localUpdatesPermeateDownstream() ? hasUpstreamUpdates() || hasLocalUpdates() : hasUpstreamUpdates(); - } - - bool updateTree(float frame, bool forceUpdates) { - if (updateContents(frame, forceUpdates)) { - return updateOutputs(frame, forceUpdates); - } else { - return false; - } - } - - /// The name of the Keypath - virtual std::string keypathName() const override { - return propertyMap()->keypathName(); - } - - /// A list of properties belonging to the keypath. - virtual std::map> keypathProperties() const override { - return propertyMap()->keypathProperties(); - } - - /// Children Keypaths - virtual std::vector> const &childKeypaths() const override { - return propertyMap()->childKeypaths(); - } - - virtual std::shared_ptr keypathLayer() const override { - return nullptr; - } - -public: - virtual LayerTransformNode *asLayerTransformNode() { - return nullptr; - } - - virtual PathNode *asPathNode() { - return nullptr; - } - - virtual RenderNode *asRenderNode() { - return nullptr; - } - -private: - std::shared_ptr _parentNode; - bool _isEnabled = true; - bool _hasLocalUpdates = false; - bool _hasUpstreamUpdates = false; - std::optional _lastUpdateFrame; -}; - -} - -#endif /* AnimatorNode_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp deleted file mode 100644 index 69d5dd81d5a..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef NodeOutput_hpp -#define NodeOutput_hpp - -#include - -#include - -namespace lottie { - -/// Defines the basic outputs of an animator node. -/// -class NodeOutput { -public: - /// The parent node. - virtual std::shared_ptr parent() = 0; - - /// Returns true if there are any updates upstream. OutputPath must be built before returning. - virtual bool hasOutputUpdates(float forFrame) = 0; - - virtual std::shared_ptr outputPath() = 0; - - virtual bool isEnabled() const = 0; - virtual void setIsEnabled(bool isEnabled) = 0; -}; - -} - -#endif /* NodeOutput_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Protocols/RenderNode.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Protocols/RenderNode.hpp deleted file mode 100644 index 5db444d408c..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/Protocols/RenderNode.hpp +++ /dev/null @@ -1,73 +0,0 @@ -#ifndef RenderNode_hpp -#define RenderNode_hpp - -#include "Lottie/Public/Primitives/CALayer.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasRenderUpdates.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasUpdate.hpp" - -namespace lottie { - -class StrokeRenderer; -class FillRenderer; -class GradientStrokeRenderer; -class GradientFillRenderer; - -/// A protocol that defines anything with render instructions -class Renderable: virtual public HasRenderUpdates, virtual public HasUpdate { -public: - enum RenderableType { - Fill, - Stroke, - GradientFill, - GradientStroke - }; - -public: - /// Determines if the renderer requires a custom context for drawing. - /// If yes the shape layer will perform a custom drawing pass. - /// If no the shape layer will be a standard CAShapeLayer - virtual bool shouldRenderInContext() = 0; - - /// Passes in the CAShapeLayer to update - virtual void updateShapeLayer(std::shared_ptr const &layer) = 0; - - /// Asks the renderer what the renderable bounds is for the given box. - virtual CGRect renderBoundsFor(CGRect const &boundingBox) { - /// Optional - return boundingBox; - } - - /// Opportunity for renderers to inject sublayers - virtual void setupSublayers(std::shared_ptr const &layer) = 0; - - virtual RenderableType renderableType() const = 0; - - virtual StrokeRenderer *asStrokeRenderer() { - return nullptr; - } - - virtual FillRenderer *asFillRenderer() { - return nullptr; - } - - virtual GradientStrokeRenderer *asGradientStrokeRenderer() { - return nullptr; - } - - virtual GradientFillRenderer *asGradientFillRenderer() { - return nullptr; - } -}; - -/// A protocol that defines a node that holds render instructions -class RenderNode { -public: - virtual std::shared_ptr renderer() = 0; - virtual std::shared_ptr nodeOutput() = 0; -}; - -} - -#endif /* RenderNode_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/RenderLayers/GetGradientParameters.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/RenderLayers/GetGradientParameters.cpp deleted file mode 100644 index a1ec1b2bed6..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/RenderLayers/GetGradientParameters.cpp +++ /dev/null @@ -1,99 +0,0 @@ -#include "GetGradientParameters.hpp" - -namespace lottie { - -void getGradientParameters(int numberOfColors, GradientColorSet const &colors, std::vector &outColors, std::vector &outLocations) { - std::vector alphaColors; - std::vector alphaValues; - std::vector alphaLocations; - - std::vector gradientColors; - std::vector colorLocations; - - for (int i = 0; i < numberOfColors; i++) { - int ix = i * 4; - if (colors.colors.size() > ix) { - Color color( - colors.colors[ix + 1], - colors.colors[ix + 2], - colors.colors[ix + 3], - 1 - ); - gradientColors.push_back(color); - colorLocations.push_back(colors.colors[ix]); - } - } - - bool drawMask = false; - for (int i = numberOfColors * 4; i < (int)colors.colors.size(); i += 2) { - float alpha = colors.colors[i + 1]; - if (alpha < 1.0) { - drawMask = true; - } - alphaLocations.push_back(colors.colors[i]); - alphaColors.push_back(Color(alpha, alpha, alpha, 1.0)); - alphaValues.push_back(alpha); - } - - if (drawMask) { - std::vector locations; - for (size_t i = 0; i < std::min(gradientColors.size(), colorLocations.size()); i++) { - if (std::find(locations.begin(), locations.end(), colorLocations[i]) == locations.end()) { - locations.push_back(colorLocations[i]); - } - } - for (size_t i = 0; i < std::min(alphaValues.size(), alphaLocations.size()); i++) { - if (std::find(locations.begin(), locations.end(), alphaLocations[i]) == locations.end()) { - locations.push_back(alphaLocations[i]); - } - } - - std::sort(locations.begin(), locations.end()); - if (locations[0] != 0.0) { - locations.insert(locations.begin(), 0.0); - } - if (locations[locations.size() - 1] != 1.0) { - locations.push_back(1.0); - } - - std::vector colors; - - for (const auto location : locations) { - Color color = gradientColors[0]; - for (size_t i = 0; i < std::min(gradientColors.size(), colorLocations.size()) - 1; i++) { - if (location >= colorLocations[i] && location <= colorLocations[i + 1]) { - float localLocation = 0.0; - if (colorLocations[i] != colorLocations[i + 1]) { - localLocation = remapFloat(location, colorLocations[i], colorLocations[i + 1], 0.0, 1.0); - } - color = ValueInterpolator::interpolate(gradientColors[i], gradientColors[i + 1], localLocation, std::nullopt, std::nullopt); - break; - } - } - - float alpha = 1.0; - for (size_t i = 0; i < std::min(alphaValues.size(), alphaLocations.size()) - 1; i++) { - if (location >= alphaLocations[i] && location <= alphaLocations[i + 1]) { - float localLocation = 0.0; - if (alphaLocations[i] != alphaLocations[i + 1]) { - localLocation = remapFloat(location, alphaLocations[i], alphaLocations[i + 1], 0.0, 1.0); - } - alpha = ValueInterpolator::interpolate(alphaValues[i], alphaValues[i + 1], localLocation, std::nullopt, std::nullopt); - break; - } - } - - color.a = alpha; - - colors.push_back(color); - } - - gradientColors = colors; - colorLocations = locations; - } - - outColors = gradientColors; - outLocations = colorLocations; -} - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/RenderLayers/GetGradientParameters.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/RenderLayers/GetGradientParameters.hpp deleted file mode 100644 index 8319b8505bc..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/NodeRenderSystem/RenderLayers/GetGradientParameters.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef ShapeRenderLayer_hpp -#define ShapeRenderLayer_hpp - -#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/RenderNode.hpp" -#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp" - -namespace lottie { - -void getGradientParameters(int numberOfColors, GradientColorSet const &colors, std::vector &outColors, std::vector &outLocations); - -} - -#endif /* ShapeRenderLayer_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Animation.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Animation.cpp deleted file mode 100644 index 577a56be354..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Animation.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "Animation.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Animation.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Animation.hpp deleted file mode 100644 index ffeb67964bf..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Animation.hpp +++ /dev/null @@ -1,314 +0,0 @@ -#ifndef Animation_hpp -#define Animation_hpp - -#include "Lottie/Public/Primitives/AnimationTime.hpp" -#include "Lottie/Private/Utility/Primitives/CoordinateSpace.hpp" -#include "Lottie/Private/Model/Layers/LayerModel.hpp" -#include "Lottie/Private/Model/Text/Glyph.hpp" -#include "Lottie/Private/Model/Text/Font.hpp" -#include "Lottie/Private/Model/Objects/Marker.hpp" -#include "Lottie/Private/Model/Assets/AssetLibrary.hpp" -#include "Lottie/Private/Model/Objects/FitzModifier.hpp" - -#include -#include "Lottie/Private/Parsing/JsonParsing.hpp" -#include "Lottie/Private/Model/Layers/LayerModelSerialization.hpp" - -#include -#include -#include -#include - -namespace lottie { - -/// The `Animation` model is the top level model object in Lottie. -/// -/// An `Animation` holds all of the animation data backing a Lottie Animation. -/// Codable, see JSON schema [here](https://github.com/airbnb/lottie-web/tree/master/docs/json). -class Animation { -public: - Animation( - std::optional name_, - std::optional tgs_, - AnimationFrameTime startFrame_, - AnimationFrameTime endFrame_, - float framerate_, - std::string const &version_, - std::optional type_, - int width_, - int height_, - std::vector> const &layers_, - std::optional>> glyphs_, - std::optional> fonts_, - std::shared_ptr assetLibrary_, - std::optional> markers_, - std::optional> fitzModifiers_, - std::optional meta_, - std::optional comps_ - ) : - startFrame(startFrame_), - endFrame(endFrame_), - framerate(framerate_), - name(name_), - version(version_), - tgs(tgs_), - type(type_), - width(width_), - height(height_), - layers(layers_), - glyphs(glyphs_), - fonts(fonts_), - assetLibrary(assetLibrary_), - markers(markers_), - fitzModifiers(fitzModifiers_), - meta(meta_), - comps(comps_) { - if (markers) { - std::map parsedMarkerMap; - for (const auto &marker : markers.value()) { - parsedMarkerMap.insert(std::make_pair(marker.name, marker)); - } - markerMap = std::move(parsedMarkerMap); - } - } - - Animation(const Animation&) = delete; - Animation& operator=(Animation&) = delete; - - static std::shared_ptr fromJson(lottiejson11::Json::object const &json) noexcept(false) { - auto name = getOptionalString(json, "nm"); - auto version = getString(json, "v"); - - auto tgs = getOptionalInt(json, "tgs"); - - std::optional type; - if (const auto typeRawValue = getOptionalInt(json, "ddd")) { - if (typeRawValue.value() == 0) { - type = CoordinateSpace::Type2d; - } else { - type = CoordinateSpace::Type3d; - } - } - - AnimationFrameTime startFrame = (float)getDouble(json, "ip"); - AnimationFrameTime endFrame = (float)getDouble(json, "op"); - - float framerate = (float)getDouble(json, "fr"); - - int width = getInt(json, "w"); - int height = getInt(json, "h"); - - auto layerDictionaries = getObjectArray(json, "layers"); - std::vector> layers; - for (size_t i = 0; i < layerDictionaries.size(); i++) { - try { - auto layer = parseLayerModel(layerDictionaries[i]); - layers.push_back(layer); - } catch(...) { - throw LottieParsingException(); - } - } - - std::optional>> glyphs; - if (const auto glyphDictionaries = getOptionalObjectArray(json, "chars")) { - glyphs = std::vector>(); - for (const auto &glyphDictionary : glyphDictionaries.value()) { - glyphs->push_back(std::make_shared(glyphDictionary)); - } - } else { - glyphs = std::nullopt; - } - - std::optional> fonts; - if (const auto fontsDictionary = getOptionalObject(json, "fonts")) { - fonts = std::make_shared(fontsDictionary.value()); - } - - std::shared_ptr assetLibrary; - if (const auto assetLibraryData = getOptionalAny(json, "assets")) { - assetLibrary = std::make_shared(assetLibraryData.value()); - } - - std::optional> markers; - if (const auto markerDictionaries = getOptionalObjectArray(json, "markers")) { - markers = std::vector(); - for (const auto &markerDictionary : markerDictionaries.value()) { - markers->push_back(Marker(markerDictionary)); - } - } - std::optional> fitzModifiers; - if (const auto fitzModifierDictionaries = getOptionalObjectArray(json, "fitz")) { - fitzModifiers = std::vector(); - for (const auto &fitzModifierDictionary : fitzModifierDictionaries.value()) { - fitzModifiers->push_back(FitzModifier(fitzModifierDictionary)); - } - } - - auto meta = getOptionalAny(json, "meta"); - auto comps = getOptionalAny(json, "comps"); - - return std::make_shared( - name, - tgs, - startFrame, - endFrame, - framerate, - version, - type, - width, - height, - std::move(layers), - std::move(glyphs), - std::move(fonts), - assetLibrary, - std::move(markers), - fitzModifiers, - meta, - comps - ); - } - - lottiejson11::Json::object toJson() const { - lottiejson11::Json::object result; - - if (name.has_value()) { - result.insert(std::make_pair("nm", name.value())); - } - - result.insert(std::make_pair("v", lottiejson11::Json(version))); - - if (tgs.has_value()) { - result.insert(std::make_pair("tgs", tgs.value())); - } - - if (type.has_value()) { - switch (type.value()) { - case CoordinateSpace::Type2d: - result.insert(std::make_pair("ddd", lottiejson11::Json(0))); - break; - case CoordinateSpace::Type3d: - result.insert(std::make_pair("ddd", lottiejson11::Json(1))); - break; - } - } - - result.insert(std::make_pair("ip", lottiejson11::Json(startFrame))); - result.insert(std::make_pair("op", lottiejson11::Json(endFrame))); - result.insert(std::make_pair("fr", lottiejson11::Json(framerate))); - result.insert(std::make_pair("w", lottiejson11::Json(width))); - result.insert(std::make_pair("h", lottiejson11::Json(height))); - - lottiejson11::Json::array layersArray; - for (const auto &layer : layers) { - lottiejson11::Json::object layerJson; - layer->toJson(layerJson); - layersArray.push_back(layerJson); - } - result.insert(std::make_pair("layers", lottiejson11::Json(layersArray))); - - if (glyphs.has_value()) { - lottiejson11::Json::array glyphArray; - for (const auto &glyph : glyphs.value()) { - glyphArray.push_back(glyph->toJson()); - } - result.insert(std::make_pair("chars", lottiejson11::Json(glyphArray))); - } - - if (fonts.has_value()) { - result.insert(std::make_pair("fonts", fonts.value()->toJson())); - } - - if (assetLibrary) { - result.insert(std::make_pair("assets", assetLibrary->toJson())); - } - - if (markers.has_value()) { - lottiejson11::Json::array markerArray; - for (const auto &marker : markers.value()) { - markerArray.push_back(marker.toJson()); - } - result.insert(std::make_pair("markers", lottiejson11::Json(markerArray))); - } - - if (fitzModifiers.has_value()) { - lottiejson11::Json::array fitzModifierArray; - for (const auto &fitzModifier : fitzModifiers.value()) { - fitzModifierArray.push_back(fitzModifier.toJson()); - } - result.insert(std::make_pair("fitz", lottiejson11::Json(fitzModifierArray))); - } - - if (meta.has_value()) { - result.insert(std::make_pair("meta", meta.value())); - } - if (comps.has_value()) { - result.insert(std::make_pair("comps", comps.value())); - } - - return result; - } - -public: - /// The start time of the composition in frameTime. - AnimationFrameTime startFrame; - - /// The end time of the composition in frameTime. - AnimationFrameTime endFrame; - - /// The frame rate of the composition. - float framerate; - - /// Return all marker names, in order, or an empty list if none are specified - std::vector markerNames() { - if (!markers.has_value()) { - return {}; - } - std::vector result; - for (const auto &marker : markers.value()) { - result.push_back(marker.name); - } - return result; - } - - /// Animation name - std::optional name; - - /// The version of the JSON Schema. - std::string version; - - std::optional tgs; - - /// The coordinate space of the composition. - std::optional type; - - /// The height of the composition in points. - int width; - - /// The width of the composition in points. - int height; - - /// The list of animation layers - std::vector> layers; - - /// The list of glyphs used for text rendering - std::optional>> glyphs; - - /// The list of fonts used for text rendering - std::optional> fonts; - - /// Asset Library - std::shared_ptr assetLibrary; - - /// Markers - std::optional> markers; - std::optional> markerMap; - - std::optional> fitzModifiers; - - std::optional meta; - std::optional comps; -}; - -} - -#endif /* Animation_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/Asset.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/Asset.cpp deleted file mode 100644 index 12f67cdd8aa..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/Asset.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "Asset.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/Asset.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/Asset.hpp deleted file mode 100644 index dabec9756cb..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/Asset.hpp +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef Asset_hpp -#define Asset_hpp - -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include -#include - -namespace lottie { - -class Asset { -public: - Asset(std::string id_) : - id(id_) { - } - - explicit Asset(lottiejson11::Json::object const &json) noexcept(false) { - auto idData = getAny(json, "id"); - if (idData.is_string()) { - id = idData.string_value(); - } else if (idData.is_number()) { - std::ostringstream idString; - idString << idData.int_value(); - id = idString.str(); - } - - objectName = getOptionalString(json, "nm"); - } - - Asset(const Asset&) = delete; - Asset& operator=(Asset&) = delete; - - virtual void toJson(lottiejson11::Json::object &json) const { - json.insert(std::make_pair("id", id)); - - if (objectName.has_value()) { - json.insert(std::make_pair("nm", objectName.value())); - } - } - -public: - /// The ID of the asset - std::string id; - - std::optional objectName; -}; - -} - -#endif /* Asset_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/AssetLibrary.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/AssetLibrary.cpp deleted file mode 100644 index 7feff3231eb..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/AssetLibrary.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "AssetLibrary.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/AssetLibrary.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/AssetLibrary.hpp deleted file mode 100644 index 5e7b893d025..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/AssetLibrary.hpp +++ /dev/null @@ -1,71 +0,0 @@ -#ifndef AssetLibrary_hpp -#define AssetLibrary_hpp - -#include "Lottie/Private/Model/Assets/Asset.hpp" -#include "Lottie/Private/Model/Assets/ImageAsset.hpp" -#include "Lottie/Private/Model/Assets/PrecompAsset.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include - -namespace lottie { - -class AssetLibrary { -public: - AssetLibrary( - std::map> const &assets_, - std::map> const &imageAssets_, - std::map> const &precompAssets_ - ) : - assets(assets_), - imageAssets(imageAssets_), - precompAssets(precompAssets_) { - } - - explicit AssetLibrary(lottiejson11::Json const &json) noexcept(false) { - if (!json.is_array()) { - throw LottieParsingException(); - } - - for (const auto &item : json.array_items()) { - if (!item.is_object()) { - throw LottieParsingException(); - } - if (item.object_items().find("layers") != item.object_items().end()) { - auto asset = std::make_shared(item.object_items()); - assets.insert(std::make_pair(asset->id, asset)); - assetList.push_back(asset); - precompAssets.insert(std::make_pair(asset->id, asset)); - } else { - auto asset = std::make_shared(item.object_items()); - assets.insert(std::make_pair(asset->id, asset)); - assetList.push_back(asset); - imageAssets.insert(std::make_pair(asset->id, asset)); - } - } - } - - lottiejson11::Json::array toJson() const { - lottiejson11::Json::array result; - - for (const auto &asset : assetList) { - lottiejson11::Json::object assetJson; - asset->toJson(assetJson); - result.push_back(assetJson); - } - - return result; - } - -public: - /// The Assets - std::vector> assetList; - std::map> assets; - - std::map> imageAssets; - std::map> precompAssets; -}; - -} - -#endif /* AssetLibrary_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/ImageAsset.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/ImageAsset.cpp deleted file mode 100644 index b5a2e57d524..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/ImageAsset.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "ImageAsset.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/ImageAsset.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/ImageAsset.hpp deleted file mode 100644 index 5eea900309e..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/ImageAsset.hpp +++ /dev/null @@ -1,125 +0,0 @@ -#ifndef ImageAsset_hpp -#define ImageAsset_hpp - -#include "Lottie/Private/Model/Assets/Asset.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -namespace lottie { - -class Image { -public: - Image() { - } -}; - -class ImageAsset: public Asset { -public: - ImageAsset( - std::string id_, - std::string name_, - std::string directory_, - float width_, - float height_ - ) : Asset(id_), - name(name_), - directory(directory_), - width(width_), - height(height_) { - } - - virtual ~ImageAsset() = default; - - explicit ImageAsset(lottiejson11::Json::object const &json) noexcept(false) : - Asset(json) { - name = getString(json, "p"); - directory = getString(json, "u"); - width = (float)getDouble(json, "w"); - height = (float)getDouble(json, "h"); - - _e = getOptionalInt(json, "e"); - _t = getOptionalString(json, "t"); - } - - virtual void toJson(lottiejson11::Json::object &json) const override { - Asset::toJson(json); - - json.insert(std::make_pair("p", name)); - json.insert(std::make_pair("u", directory)); - json.insert(std::make_pair("w", width)); - json.insert(std::make_pair("h", height)); - - if (_e.has_value()) { - json.insert(std::make_pair("e", _e.value())); - } - if (_t.has_value()) { - json.insert(std::make_pair("t", _t.value())); - } - } - -public: - /// Image name - std::string name; - - /// Image Directory - std::string directory; - - /// Image Size - float width; - float height; - - std::optional _e; - std::optional _t; -}; - -/*extension Data { - - // MARK: Lifecycle - - /// Initializes `Data` from an `ImageAsset`. - /// - /// Returns nil when the input is not recognized as valid Data URL. - /// - parameter imageAsset: The image asset that contains Data URL. - internal init?(imageAsset: ImageAsset) { - self.init(dataString: imageAsset.name) - } - - /// Initializes `Data` from a [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) String. - /// - /// Returns nil when the input is not recognized as valid Data URL. - /// - parameter dataString: The data string to parse. - /// - parameter options: Options for the string parsing. Default value is `[]`. - internal init?(dataString: String, options: DataURLReadOptions = []) { - guard - dataString.hasPrefix("data:"), - let url = URL(string: dataString) - else { - return nil - } - // The code below is needed because Data(contentsOf:) floods logs - // with messages since url doesn't have a host. This only fixes flooding logs - // when data inside Data URL is base64 encoded. - if - let base64Range = dataString.range(of: ";base64,"), - !options.contains(DataURLReadOptions.legacy) - { - let encodedString = String(dataString[base64Range.upperBound...]) - self.init(base64Encoded: encodedString) - } else { - try? self.init(contentsOf: url) - } - } - - // MARK: Internal - - internal struct DataURLReadOptions: OptionSet { - let rawValue: Int - - /// Will read Data URL using Data(contentsOf:) - static let legacy = DataURLReadOptions(rawValue: 1 << 0) - } - -};*/ - -} - -#endif /* ImageAsset_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/PrecompAsset.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/PrecompAsset.cpp deleted file mode 100644 index 976044565aa..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/PrecompAsset.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "PrecompAsset.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/PrecompAsset.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/PrecompAsset.hpp deleted file mode 100644 index af169474a7b..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Assets/PrecompAsset.hpp +++ /dev/null @@ -1,66 +0,0 @@ -#ifndef PrecompAsset_hpp -#define PrecompAsset_hpp - -#include "Lottie/Private/Model/Assets/Asset.hpp" -#include "Lottie/Private/Model/Layers/LayerModel.hpp" -#include "Lottie/Private/Model/Layers/LayerModelSerialization.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include - -namespace lottie { - -class PrecompAsset: public Asset { -public: - PrecompAsset( - std::string const &id_, - std::vector> const &layers_ - ) : Asset(id_), - layers(layers_) { - } - - virtual ~PrecompAsset() = default; - - explicit PrecompAsset(lottiejson11::Json::object const &json) noexcept(false) : - Asset(json) { - if (const auto frameRateValue = getOptionalDouble(json, "fr")) { - frameRate = (float)frameRateValue.value(); - } - - auto layerDictionaries = getObjectArray(json, "layers"); - for (size_t i = 0; i < layerDictionaries.size(); i++) { - try { - auto layer = parseLayerModel(layerDictionaries[i]); - layers.push_back(layer); - } catch(...) { - throw LottieParsingException(); - } - } - } - - virtual void toJson(lottiejson11::Json::object &json) const override { - Asset::toJson(json); - - lottiejson11::Json::array layerArray; - for (const auto &layer : layers) { - lottiejson11::Json::object layerJson; - layer->toJson(layerJson); - layerArray.push_back(layerJson); - } - json.insert(std::make_pair("layers", layerArray)); - - if (frameRate.has_value()) { - json.insert(std::make_pair("fr", frameRate.value())); - } - } - -public: - /// Layers of the precomp - std::vector> layers; - - std::optional frameRate; -}; - -} - -#endif /* PrecompAsset_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Keyframes/KeyframeGroup.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Keyframes/KeyframeGroup.cpp deleted file mode 100644 index 2f0c1a788a7..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Keyframes/KeyframeGroup.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "KeyframeGroup.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Keyframes/KeyframeGroup.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Keyframes/KeyframeGroup.hpp deleted file mode 100644 index 50549a1121c..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Keyframes/KeyframeGroup.hpp +++ /dev/null @@ -1,150 +0,0 @@ -#ifndef KeyframeGroup_hpp -#define KeyframeGroup_hpp - -#include "Lottie/Public/Keyframes/Keyframe.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include - -namespace lottie { - -/// Used for coding/decoding a group of Keyframes by type. -/// -/// Keyframe data is wrapped in a dictionary { "k" : KeyframeData }. -/// The keyframe data can either be an array of keyframes or, if no animation is present, the raw value. -/// This helper object is needed to properly decode the json. - -template -class KeyframeGroup { -public: - KeyframeGroup(std::vector> &&keyframes_) : - keyframes(std::move(keyframes_)), - isSingle(false) { - } - - KeyframeGroup(T const &value_) : - keyframes({ Keyframe(value_, std::nullopt, std::nullopt) }), - isSingle(false) { - } - - KeyframeGroup(lottiejson11::Json::object const &json) noexcept(false) { - isAnimated = getOptionalInt(json, "a"); - expression = getOptionalAny(json, "x"); - expressionIndex = getOptionalInt(json, "ix"); - _extraL = getOptionalInt(json, "l"); - - auto containerData = getAny(json, "k"); - - try { - LottieParsingException::Guard expectedException; - T keyframeData = T(containerData); - keyframes.push_back(Keyframe(keyframeData, std::nullopt, std::nullopt)); - isSingle = true; - } catch(...) { - // Decode and array of keyframes. - // - // Body Movin and Lottie deal with keyframes in different ways. - // - // A keyframe object in Body movin defines a span of time with a START - // and an END, from the current keyframe time to the next keyframe time. - // - // A keyframe object in Lottie defines a singular point in time/space. - // This point has an in-tangent and an out-tangent. - // - // To properly decode this we must iterate through keyframes while holding - // reference to the previous keyframe. - - if (!containerData.is_array()) { - throw LottieParsingException(); - } - - std::optional> previousKeyframeData; - for (const auto &containerItem : containerData.array_items()) { - // Ensure that Time and Value are present. - auto keyframeData = KeyframeData(containerItem); - rawKeyframeData.push_back(keyframeData); - - std::optional value; - if (keyframeData.startValue.has_value()) { - value = keyframeData.startValue; - } else if (previousKeyframeData.has_value()) { - value = previousKeyframeData->endValue; - } - if (!value.has_value()) { - throw LottieParsingException(); - } - if (!keyframeData.time.has_value()) { - throw LottieParsingException(); - } - - std::optional inTangent; - std::optional spatialInTangent; - if (previousKeyframeData.has_value()) { - inTangent = previousKeyframeData->inTangent; - spatialInTangent = previousKeyframeData->spatialInTangent; - } - - keyframes.emplace_back( - value.value(), - keyframeData.time.value(), - keyframeData.isHold(), - inTangent, - keyframeData.outTangent, - spatialInTangent, - keyframeData.spatialOutTangent - ); - - previousKeyframeData = keyframeData; - } - - isSingle = false; - } - } - - lottiejson11::Json::object toJson() const { - lottiejson11::Json::object result; - - assert(!keyframes.empty()); - - if (keyframes.size() == 1 && isSingle) { - result.insert(std::make_pair("k", keyframes[0].value.toJson())); - } else { - lottiejson11::Json::array containerData; - - for (const auto &keyframe : rawKeyframeData) { - containerData.push_back(keyframe.toJson()); - } - - result.insert(std::make_pair("k", containerData)); - } - - if (isAnimated.has_value()) { - result.insert(std::make_pair("a", isAnimated.value())); - } - if (expression.has_value()) { - result.insert(std::make_pair("x", expression.value())); - } - if (expressionIndex.has_value()) { - result.insert(std::make_pair("ix", expressionIndex.value())); - } - if (_extraL.has_value()) { - result.insert(std::make_pair("l", _extraL.value())); - } - - return result; - } - -public: - std::vector> keyframes; - std::optional isAnimated; - - std::optional expression; - std::optional expressionIndex; - std::vector> rawKeyframeData; - bool isSingle = false; - std::optional _extraL; -}; - -} - -#endif /* KeyframeGroup_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ImageLayerModel.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ImageLayerModel.cpp deleted file mode 100644 index 4251973760c..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ImageLayerModel.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "ImageLayerModel.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ImageLayerModel.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ImageLayerModel.hpp deleted file mode 100644 index 23349f73904..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ImageLayerModel.hpp +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef ImageLayerModel_hpp -#define ImageLayerModel_hpp - -#include "Lottie/Private/Model/Layers/LayerModel.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -namespace lottie { - -/// A layer that holds an image. -class ImageLayerModel: public LayerModel { -public: - explicit ImageLayerModel(lottiejson11::Json::object const &json) noexcept(false) : - LayerModel(json) { - referenceID = getString(json, "refId"); - - _sc = getOptionalString(json, "sc"); - } - - virtual ~ImageLayerModel() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - LayerModel::toJson(json); - - json.insert(std::make_pair("refId", referenceID)); - - if (_sc.has_value()) { - json.insert(std::make_pair("sc", _sc.value())); - } - } - -public: - /// The reference ID of the image. - std::string referenceID; - - std::optional _sc; -}; - -} - -#endif /* ImageLayerModel_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModel.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModel.cpp deleted file mode 100644 index f14f1df9b8c..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModel.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "LayerModel.hpp" - -namespace lottie { - -LayerType parseLayerType(lottiejson11::Json::object const &json, std::string const &key) { - if (const auto layerTypeValue = getOptionalInt(json, "ty")) { - switch (layerTypeValue.value()) { - case 0: - return LayerType::Precomp; - case 1: - return LayerType::Solid; - case 2: - return LayerType::Image; - case 3: - return LayerType::Null; - case 4: - return LayerType::Shape; - case 5: - return LayerType::Text; - default: - return LayerType::Null; - } - } else { - return LayerType::Null; - } -} - -int serializeLayerType(LayerType value) { - switch (value) { - case LayerType::Precomp: - return 0; - case LayerType::Solid: - return 1; - case LayerType::Image: - return 2; - case LayerType::Null: - return 3; - case LayerType::Shape: - return 4; - case LayerType::Text: - return 5; - } -} - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModel.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModel.hpp deleted file mode 100644 index 16f84d0f10f..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModel.hpp +++ /dev/null @@ -1,318 +0,0 @@ -#ifndef LayerModel_hpp -#define LayerModel_hpp - -#include "Lottie/Private/Model/Objects/Transform.hpp" -#include "Lottie/Private/Model/Objects/Mask.hpp" -#include "Lottie/Private/Utility/Primitives/CoordinateSpace.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include -#include -#include - -namespace lottie { - -enum class LayerType { - Precomp, - Solid, - Image, - Null, - Shape, - Text -}; - -LayerType parseLayerType(lottiejson11::Json::object const &json, std::string const &key); -int serializeLayerType(LayerType value); - -enum class MatteType: int { - None = 0, - Add = 1, - Invert = 2, - Unknown = 3 -}; - -enum class BlendMode: int { - Normal = 0, - Multiply = 1, - Screen = 2, - Overlay = 3, - Darken = 4, - Lighten = 5, - ColorDodge = 6, - ColorBurn = 7, - HardLight = 8, - SoftLight = 9, - Difference = 10, - Exclusion = 11, - Hue = 12, - Saturation = 13, - Color = 14, - Luminosity = 15 -}; - -/// A base top container for shapes, images, and other view objects. -class LayerModel { -public: - explicit LayerModel(lottiejson11::Json::object const &json) noexcept(false) { - name = getOptionalString(json, "nm"); - index = getOptionalInt(json, "ind"); - - type = parseLayerType(json, "ty"); - - autoOrient = getOptionalInt(json, "ao"); - - if (const auto typeRawValue = getOptionalInt(json, "ddd")) { - if (typeRawValue.value() == 0) { - coordinateSpace = CoordinateSpace::Type2d; - } else { - coordinateSpace = CoordinateSpace::Type3d; - } - } else { - coordinateSpace = std::nullopt; - } - - inFrame = (float)getDouble(json, "ip"); - outFrame = (float)getDouble(json, "op"); - startTime = (float)getDouble(json, "st"); - - transform = std::make_shared(getObject(json, "ks")); - parent = getOptionalInt(json, "parent"); - - if (const auto blendModeRawValue = getOptionalInt(json, "bm")) { - switch (blendModeRawValue.value()) { - case 0: - blendMode = BlendMode::Normal; - break; - case 1: - blendMode = BlendMode::Multiply; - break; - case 2: - blendMode = BlendMode::Screen; - break; - case 3: - blendMode = BlendMode::Overlay; - break; - case 4: - blendMode = BlendMode::Darken; - break; - case 5: - blendMode = BlendMode::Lighten; - break; - case 6: - blendMode = BlendMode::ColorDodge; - break; - case 7: - blendMode = BlendMode::ColorBurn; - break; - case 8: - blendMode = BlendMode::HardLight; - break; - case 9: - blendMode = BlendMode::SoftLight; - break; - case 10: - blendMode = BlendMode::Difference; - break; - case 11: - blendMode = BlendMode::Exclusion; - break; - case 12: - blendMode = BlendMode::Hue; - break; - case 13: - blendMode = BlendMode::Saturation; - break; - case 14: - blendMode = BlendMode::Color; - break; - case 15: - blendMode = BlendMode::Luminosity; - break; - default: - throw LottieParsingException(); - } - } - - if (const auto maskDictionaries = getOptionalObjectArray(json, "masksProperties")) { - masks = std::vector>(); - for (const auto &maskDictionary : maskDictionaries.value()) { - masks->push_back(std::make_shared(maskDictionary)); - } - } - - if (const auto timeStretchData = getOptionalDouble(json, "sr")) { - _timeStretch = (float)timeStretchData.value(); - } - - if (const auto matteRawValue = getOptionalInt(json, "tt")) { - switch (matteRawValue.value()) { - case 0: - matte = MatteType::None; - break; - case 1: - matte = MatteType::Add; - break; - case 2: - matte = MatteType::Invert; - break; - case 3: - matte = MatteType::Unknown; - break; - default: - throw LottieParsingException(); - } - } - - if (const auto hiddenData = getOptionalBool(json, "hd")) { - hidden = hiddenData.value(); - } - - hasMask = getOptionalBool(json, "hasMask"); - td = getOptionalInt(json, "td"); - effectsData = getOptionalAny(json, "ef"); - layerClass = getOptionalString(json, "cl"); - _extraHidden = getOptionalAny(json, "hidden"); - } - - LayerModel(const LayerModel&) = delete; - LayerModel& operator=(LayerModel&) = delete; - - virtual ~LayerModel() = default; - - virtual void toJson(lottiejson11::Json::object &json) const { - if (name.has_value()) { - json.insert(std::make_pair("nm", name.value())); - } - if (index.has_value()) { - json.insert(std::make_pair("ind", index.value())); - } - - if (autoOrient.has_value()) { - json.insert(std::make_pair("ao", autoOrient.value())); - } - - json.insert(std::make_pair("ty", serializeLayerType(type))); - - if (coordinateSpace.has_value()) { - switch (coordinateSpace.value()) { - case CoordinateSpace::Type2d: - json.insert(std::make_pair("ddd", 0)); - break; - case CoordinateSpace::Type3d: - json.insert(std::make_pair("ddd", 1)); - break; - } - } - - json.insert(std::make_pair("ip", inFrame)); - json.insert(std::make_pair("op", outFrame)); - json.insert(std::make_pair("st", startTime)); - - json.insert(std::make_pair("ks", transform->toJson())); - - if (parent.has_value()) { - json.insert(std::make_pair("parent", parent.value())); - } - - if (blendMode.has_value()) { - json.insert(std::make_pair("bm", (int)blendMode.value())); - } - - if (masks.has_value()) { - lottiejson11::Json::array maskArray; - for (const auto &mask : masks.value()) { - maskArray.push_back(mask->toJson()); - } - json.insert(std::make_pair("masksProperties", maskArray)); - } - - if (_timeStretch.has_value()) { - json.insert(std::make_pair("sr", _timeStretch.value())); - } - - if (matte.has_value()) { - json.insert(std::make_pair("tt", (int)matte.value())); - } - - if (hidden.has_value()) { - json.insert(std::make_pair("hd", hidden.value())); - } - - if (hasMask.has_value()) { - json.insert(std::make_pair("hasMask", hasMask.value())); - } - if (td.has_value()) { - json.insert(std::make_pair("td", td.value())); - } - if (effectsData.has_value()) { - json.insert(std::make_pair("ef", effectsData.value())); - } - if (layerClass.has_value()) { - json.insert(std::make_pair("cl", layerClass.value())); - } - if (_extraHidden.has_value()) { - json.insert(std::make_pair("hidden", _extraHidden.value())); - } - } - - float timeStretch() { - if (_timeStretch.has_value()) { - return _timeStretch.value(); - } else { - return 1.0; - } - } - -public: - /// The readable name of the layer - std::optional name; - - /// The index of the layer - std::optional index; - - /// The type of the layer. - LayerType type; - - std::optional autoOrient; - - /// The coordinate space - std::optional coordinateSpace; - - /// The in time of the layer in frames. - float inFrame; - /// The out time of the layer in frames. - float outFrame; - - /// The start time of the layer in frames. - float startTime; - - /// The transform of the layer - std::shared_ptr transform; - - /// The index of the parent layer, if applicable. - std::optional parent; - - /// The blending mode for the layer - std::optional blendMode; - - /// An array of masks for the layer. - std::optional>> masks; - - /// A number that stretches time by a multiplier - std::optional _timeStretch; - - /// The type of matte if any. - std::optional matte; - - std::optional hidden; - - std::optional hasMask; - std::optional td; - std::optional effectsData; - std::optional layerClass; - std::optional _extraHidden; -}; - -} - -#endif /* LayerModel_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.cpp deleted file mode 100644 index 47bb5b1467a..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "LayerModelSerialization.hpp" - -#include "Lottie/Private/Model/Layers/PreCompLayerModel.hpp" -#include "Lottie/Private/Model/Layers/SolidLayerModel.hpp" -#include "Lottie/Private/Model/Layers/ImageLayerModel.hpp" -#include "Lottie/Private/Model/Layers/ShapeLayerModel.hpp" -#include "Lottie/Private/Model/Layers/TextLayerModel.hpp" - -namespace lottie { - -std::shared_ptr parseLayerModel(lottiejson11::Json::object const &json) noexcept(false) { - LayerType layerType = parseLayerType(json, "ty"); - - switch (layerType) { - case LayerType::Precomp: - return std::make_shared(json); - case LayerType::Solid: - return std::make_shared(json); - case LayerType::Image: - return std::make_shared(json); - case LayerType::Null: - return std::make_shared(json); - case LayerType::Shape: - try { - return std::make_shared(json); - } catch(...) { - throw LottieParsingException(); - } - case LayerType::Text: - return std::make_shared(json); - } -} - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.hpp deleted file mode 100644 index cc2f9ee012e..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/LayerModelSerialization.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef LayerModelSerialization_hpp -#define LayerModelSerialization_hpp - -#include -#include "Lottie/Private/Parsing/JsonParsing.hpp" -#include "Lottie/Private/Model/Layers/LayerModel.hpp" - -namespace lottie { - -std::shared_ptr parseLayerModel(lottiejson11::Json::object const &json) noexcept(false); - -} - -#endif /* LayerModelSerialization_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/PreCompLayerModel.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/PreCompLayerModel.cpp deleted file mode 100644 index 2a4c7b1363a..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/PreCompLayerModel.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "PreCompLayerModel.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/PreCompLayerModel.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/PreCompLayerModel.hpp deleted file mode 100644 index 16a2195a0ba..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/PreCompLayerModel.hpp +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef PreCompLayerModel_hpp -#define PreCompLayerModel_hpp - -#include "Lottie/Private/Model/Layers/LayerModel.hpp" -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include - -namespace lottie { - -/// A layer that holds another animation composition. -class PreCompLayerModel: public LayerModel { -public: - PreCompLayerModel(lottiejson11::Json::object const &json) : - LayerModel(json) { - referenceID = getString(json, "refId"); - if (const auto timeRemappingData = getOptionalObject(json, "tm")) { - timeRemapping = KeyframeGroup(timeRemappingData.value()); - } - width = (float)getDouble(json, "w"); - height = (float)getDouble(json, "h"); - } - - virtual ~PreCompLayerModel() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - LayerModel::toJson(json); - - json.insert(std::make_pair("refId", referenceID)); - - if (timeRemapping.has_value()) { - json.insert(std::make_pair("tm", timeRemapping->toJson())); - } - - json.insert(std::make_pair("w", width)); - json.insert(std::make_pair("h", height)); - } - -public: - /// The reference ID of the precomp. - std::string referenceID; - - /// A value that remaps time over time. - std::optional> timeRemapping; - - /// Precomp Width - float width; - - /// Precomp Height - float height; -}; - -} - -#endif /* PreCompLayerModel_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ShapeLayerModel.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ShapeLayerModel.cpp deleted file mode 100644 index 608dca567bf..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ShapeLayerModel.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "ShapeLayerModel.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ShapeLayerModel.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ShapeLayerModel.hpp deleted file mode 100644 index 61f4329bbaa..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/ShapeLayerModel.hpp +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef ShapeLayerModel_hpp -#define ShapeLayerModel_hpp - -#include "Lottie/Private/Model/Layers/LayerModel.hpp" -#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include - -namespace lottie { - -/// A layer that holds vector shape objects. -class ShapeLayerModel: public LayerModel { -public: - ShapeLayerModel(lottiejson11::Json::object const &json) noexcept(false) : - LayerModel(json) { - auto shapeItemsData = getObjectArray(json, "shapes"); - for (const auto &shapeItemData : shapeItemsData) { - items.push_back(parseShapeItem(shapeItemData)); - } - } - - virtual ~ShapeLayerModel() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - LayerModel::toJson(json); - - lottiejson11::Json::array shapeItemArray; - for (const auto &item : items) { - lottiejson11::Json::object itemJson; - item->toJson(itemJson); - shapeItemArray.push_back(itemJson); - } - - json.insert(std::make_pair("shapes", shapeItemArray)); - } - -public: - /// A list of shape items. - std::vector> items; -}; - -} - -#endif /* ShapeLayerModel_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/SolidLayerModel.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/SolidLayerModel.cpp deleted file mode 100644 index 153c2839532..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/SolidLayerModel.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "SolidLayerModel.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/SolidLayerModel.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/SolidLayerModel.hpp deleted file mode 100644 index cea0671b49d..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/SolidLayerModel.hpp +++ /dev/null @@ -1,42 +0,0 @@ -#ifndef SolidLayerModel_hpp -#define SolidLayerModel_hpp - -#include "Lottie/Private/Model/Layers/LayerModel.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -namespace lottie { - -/// A layer that holds a solid color. -class SolidLayerModel: public LayerModel { -public: - explicit SolidLayerModel(lottiejson11::Json::object const &json) noexcept(false) : - LayerModel(json) { - colorHex = getString(json, "sc"); - width = (float)getDouble(json, "sw"); - height = (float)getDouble(json, "sh"); - } - - virtual ~SolidLayerModel() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - LayerModel::toJson(json); - - json.insert(std::make_pair("sc", colorHex)); - json.insert(std::make_pair("sw", width)); - json.insert(std::make_pair("sh", height)); - } - -public: - /// The color of the solid in Hex // Change to value provider. - std::string colorHex; - - /// The Width of the color layer - float width; - - /// The height of the color layer - float height; -}; - -} - -#endif /* SolidLayerModel_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/TextLayerModel.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/TextLayerModel.cpp deleted file mode 100644 index 9e2ad034b15..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/TextLayerModel.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "TextLayerModel.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/TextLayerModel.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/TextLayerModel.hpp deleted file mode 100644 index 37fb374ddf6..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Layers/TextLayerModel.hpp +++ /dev/null @@ -1,83 +0,0 @@ -#ifndef TextLayerModel_hpp -#define TextLayerModel_hpp - -#include "Lottie/Private/Model/Layers/LayerModel.hpp" -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include "Lottie/Private/Model/Text/TextDocument.hpp" -#include "Lottie/Private/Model/Text/TextAnimator.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -namespace lottie { - -/// A layer that holds text. -class TextLayerModel: public LayerModel { -public: - TextLayerModel(lottiejson11::Json::object const &json) : - LayerModel(json), - text(KeyframeGroup(TextDocument( - "", - 0.0, - "", - TextJustification::Left, - 0, - 0.0, - std::nullopt, - std::nullopt, - std::nullopt, - std::nullopt, - std::nullopt, - std::nullopt, - std::nullopt - ))) { - auto textContainer = getObject(json, "t"); - - auto textData = getObject(textContainer, "d"); - text = KeyframeGroup(textData); - - if (auto animatorsData = getOptionalObjectArray(textContainer, "a")) { - for (const auto &animatorData : animatorsData.value()) { - animators.push_back(std::make_shared(animatorData)); - } - } - - _extraM = getOptionalAny(textContainer, "m"); - _extraP = getOptionalAny(textContainer, "p"); - } - - virtual ~TextLayerModel() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - LayerModel::toJson(json); - - lottiejson11::Json::object textContainer; - textContainer.insert(std::make_pair("d", text.toJson())); - if (_extraM.has_value()) { - textContainer.insert(std::make_pair("m", _extraM.value())); - } - if (_extraP.has_value()) { - textContainer.insert(std::make_pair("p", _extraP.value())); - } - lottiejson11::Json::array animatorArray; - for (const auto &animator : animators) { - animatorArray.push_back(animator->toJson()); - } - textContainer.insert(std::make_pair("a", animatorArray)); - - json.insert(std::make_pair("t", textContainer)); - } - -public: - /// The text for the layer - KeyframeGroup text; - - /// Text animators - std::vector> animators; - - std::optional _extraM; - std::optional _extraP; - std::optional _extraA; -}; - -} - -#endif /* TextLayerModel_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/DashElement.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/DashElement.cpp deleted file mode 100644 index a28df197702..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/DashElement.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "DashElement.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/DashElement.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/DashElement.hpp deleted file mode 100644 index 87379ea7224..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/DashElement.hpp +++ /dev/null @@ -1,78 +0,0 @@ -#ifndef DashElement_hpp -#define DashElement_hpp - -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" -#include "Lottie/Public/Primitives/DashPattern.hpp" - -namespace lottie { - -enum class DashElementType { - Offset, - Dash, - Gap -}; - -class DashElement { -public: - DashElement( - DashElementType type_, - KeyframeGroup const &value_ - ) : - type(type_), - value(value_) { - } - - explicit DashElement(lottiejson11::Json::object const &json) noexcept(false) : - type(DashElementType::Offset), - value(KeyframeGroup(Vector1D(0.0))) { - auto typeRawValue = getString(json, "n"); - if (typeRawValue == "o") { - type = DashElementType::Offset; - } else if (typeRawValue == "d") { - type = DashElementType::Dash; - } else if (typeRawValue == "g") { - type = DashElementType::Gap; - } else { - throw LottieParsingException(); - } - - value = KeyframeGroup(getObject(json, "v")); - - name = getOptionalString(json, "nm"); - } - - lottiejson11::Json::object toJson() const { - lottiejson11::Json::object result; - - switch (type) { - case DashElementType::Offset: - result.insert(std::make_pair("n", "o")); - break; - case DashElementType::Dash: - result.insert(std::make_pair("n", "d")); - break; - case DashElementType::Gap: - result.insert(std::make_pair("n", "g")); - break; - } - - result.insert(std::make_pair("v", value.toJson())); - - if (name.has_value()) { - result.insert(std::make_pair("nm", name.value())); - } - - return result; - } - -public: - DashElementType type; - KeyframeGroup value; - - std::optional name; -}; - -} - -#endif /* DashElement_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/FitzModifier.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/FitzModifier.cpp deleted file mode 100644 index 7b18e01e4a3..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/FitzModifier.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "FitzModifier.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/FitzModifier.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/FitzModifier.hpp deleted file mode 100644 index 632a2374f93..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/FitzModifier.hpp +++ /dev/null @@ -1,53 +0,0 @@ -#ifndef FitzModifier_hpp -#define FitzModifier_hpp - -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -namespace lottie { - -class FitzModifier { -public: - explicit FitzModifier(lottiejson11::Json::object const &json) noexcept(false) { - original = getInt(json, "o"); - type12 = getOptionalInt(json, "f12"); - type3 = getOptionalInt(json, "f3"); - type4 = getOptionalInt(json, "f4"); - type5 = getOptionalInt(json, "f5"); - type6 = getOptionalInt(json, "f6"); - } - - lottiejson11::Json::object toJson() const { - lottiejson11::Json::object result; - - result.insert(std::make_pair("o", (float)original)); - if (type12.has_value()) { - result.insert(std::make_pair("f12", (float)type12.value())); - } - if (type3.has_value()) { - result.insert(std::make_pair("f3", (float)type3.value())); - } - if (type4.has_value()) { - result.insert(std::make_pair("f4", (float)type4.value())); - } - if (type5.has_value()) { - result.insert(std::make_pair("f5", (float)type5.value())); - } - if (type6.has_value()) { - result.insert(std::make_pair("f6", (float)type6.value())); - } - - return result; - } - -public: - float original; - std::optional type12; - std::optional type3; - std::optional type4; - std::optional type5; - std::optional type6; -}; - -} - -#endif /* FitzModifier_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Marker.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Marker.cpp deleted file mode 100644 index ea2664d4162..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Marker.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "Marker.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Marker.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Marker.hpp deleted file mode 100644 index bd1e5584dd3..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Marker.hpp +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef Marker_hpp -#define Marker_hpp - -#include "Lottie/Public/Primitives/AnimationTime.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include - -namespace lottie { - -class Marker { -public: - Marker( - std::string const &name_, - AnimationFrameTime frameTime_ - ) : - name(name_), - frameTime(frameTime_) { - } - - explicit Marker(lottiejson11::Json::object const &json) noexcept(false) { - name = getString(json, "cm"); - frameTime = (float)getDouble(json, "tm"); - dr = getOptionalInt(json, "dr"); - } - - lottiejson11::Json::object toJson() const { - lottiejson11::Json::object result; - - result.insert(std::make_pair("cm", name)); - result.insert(std::make_pair("tm", frameTime)); - - if (dr.has_value()) { - result.insert(std::make_pair("dr", dr.value())); - } - - return result; - } - -public: - /// The Marker Name - std::string name; - - /// The Frame time of the marker - AnimationFrameTime frameTime; - - std::optional dr; -}; - -} - -#endif /* Marker_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Mask.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Mask.cpp deleted file mode 100644 index 8cb64d67095..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Mask.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "Mask.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Mask.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Mask.hpp deleted file mode 100644 index 0a4d790684f..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Mask.hpp +++ /dev/null @@ -1,139 +0,0 @@ -#ifndef Mask_hpp -#define Mask_hpp - -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -namespace lottie { - -enum class MaskMode { - Add, - Subtract, - Intersect, - Lighten, - Darken, - Difference, - None -}; - -class Mask { -public: - explicit Mask(lottiejson11::Json::object const &json) noexcept(false) : - opacity(KeyframeGroup(Vector1D(100.0))), - shape(KeyframeGroup(BezierPath())), - inverted(false), - expansion(KeyframeGroup(Vector1D(0.0))) { - if (const auto modeRawValue = getOptionalString(json, "mode")) { - if (modeRawValue.value() == "a") { - _mode = MaskMode::Add; - } else if (modeRawValue.value() == "s") { - _mode = MaskMode::Subtract; - } else if (modeRawValue.value() == "i") { - _mode = MaskMode::Intersect; - } else if (modeRawValue.value() == "l") { - _mode = MaskMode::Lighten; - } else if (modeRawValue.value() == "d") { - _mode = MaskMode::Darken; - } else if (modeRawValue.value() == "f") { - _mode = MaskMode::Difference; - } else if (modeRawValue.value() == "n") { - _mode = MaskMode::None; - } else { - throw LottieParsingException(); - } - } - - if (const auto opacityData = getOptionalObject(json, "o")) { - opacity = KeyframeGroup(opacityData.value()); - } - - shape = KeyframeGroup(getObject(json, "pt")); - - if (const auto invertedData = getOptionalBool(json, "inv")) { - inverted = invertedData.value(); - } - - if (const auto expansionData = getOptionalObject(json, "x")) { - expansion = KeyframeGroup(expansionData.value()); - } - - name = getOptionalString(json, "nm"); - } - - lottiejson11::Json::object toJson() const { - lottiejson11::Json::object result; - - if (_mode.has_value()) { - switch (_mode.value()) { - case MaskMode::Add: - result.insert(std::make_pair("mode", "a")); - break; - case MaskMode::Subtract: - result.insert(std::make_pair("mode", "s")); - break; - case MaskMode::Intersect: - result.insert(std::make_pair("mode", "i")); - break; - case MaskMode::Lighten: - result.insert(std::make_pair("mode", "l")); - break; - case MaskMode::Darken: - result.insert(std::make_pair("mode", "d")); - break; - case MaskMode::Difference: - result.insert(std::make_pair("mode", "f")); - break; - case MaskMode::None: - result.insert(std::make_pair("mode", "n")); - break; - } - } - - if (opacity.has_value()) { - result.insert(std::make_pair("o", opacity->toJson())); - } - - result.insert(std::make_pair("pt", shape.toJson())); - - if (inverted.has_value()) { - result.insert(std::make_pair("inv", inverted.value())); - } - - if (expansion.has_value()) { - result.insert(std::make_pair("x", expansion->toJson())); - } - - if (name.has_value()) { - result.insert(std::make_pair("nm", name.value())); - } - - return result; - } - -public: - MaskMode mode() const { - if (_mode.has_value()) { - return _mode.value(); - } else { - return MaskMode::Add; - } - } - -public: - std::optional _mode; - - std::optional> opacity; - - KeyframeGroup shape; - - std::optional inverted; - - std::optional> expansion; - - std::optional name; -}; - -} - -#endif /* Mask_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Transform.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Transform.cpp deleted file mode 100644 index e55154304fc..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Transform.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "Transform.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Transform.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Transform.hpp deleted file mode 100644 index 19cfc92da83..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Objects/Transform.hpp +++ /dev/null @@ -1,267 +0,0 @@ -#ifndef Transform_hpp -#define Transform_hpp - -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include - -namespace lottie { - -class Transform { -public: - enum class PositionInternalRepresentation { - None, - TopLevelXY, - TopLevelCombined, - NestedXY - }; - - enum class RotationZInternalRepresentation { - RZ, - R - }; - -public: - Transform( - std::optional> anchorPoint_, - std::optional> position_, - std::optional> positionX_, - std::optional> positionY_, - std::optional> scale_, - std::optional> rotation_, - std::optional> &opacity_, - std::optional> rotationZ_ - ) : - _anchorPoint(anchorPoint_), - _position(position_), - _positionX(positionX_), - _positionY(positionY_), - _scale(scale_), - _rotation(rotation_), - _opacity(opacity_), - _rotationZ(rotationZ_) { - } - - explicit Transform(lottiejson11::Json::object const &json) noexcept(false) { - // AnchorPoint - if (const auto anchorPointDictionary = getOptionalObject(json, "a")) { - _anchorPoint = KeyframeGroup(anchorPointDictionary.value()); - } - - try { - auto xDictionary = getOptionalObject(json, "px"); - auto yDictionary = getOptionalObject(json, "py"); - if (xDictionary.has_value() && yDictionary.has_value()) { - _positionX = KeyframeGroup(xDictionary.value()); - _positionY = KeyframeGroup(yDictionary.value()); - _position = std::nullopt; - _positionInternalRepresentation = PositionInternalRepresentation::TopLevelXY; - } else if (const auto positionData = getOptionalObject(json, "p")) { - try { - LottieParsingException::Guard expectedGuard; - - _position = KeyframeGroup(positionData.value()); - _positionX = std::nullopt; - _positionX = std::nullopt; - _positionInternalRepresentation = PositionInternalRepresentation::TopLevelCombined; - } catch(...) { - auto xData = getObject(positionData.value(), "x"); - auto yData = getObject(positionData.value(), "y"); - _positionX = KeyframeGroup(xData); - _positionY = KeyframeGroup(yData); - _position = std::nullopt; - _positionInternalRepresentation = PositionInternalRepresentation::NestedXY; - _extra_positionS = getOptionalBool(positionData.value(), "s"); - } - } else { - _position = std::nullopt; - _positionX = std::nullopt; - _positionY = std::nullopt; - _positionInternalRepresentation = PositionInternalRepresentation::None; - } - } catch(...) { - throw LottieParsingException(); - } - - try { - // Scale - if (const auto scaleData = getOptionalObject(json, "s")) { - _scale = KeyframeGroup(scaleData.value()); - } - - // Rotation - if (const auto rotationZData = getOptionalObject(json, "rz")) { - _rotationZ = KeyframeGroup(rotationZData.value()); - _rotationZInternalRepresentation = RotationZInternalRepresentation::RZ; - } else if (const auto rotationData = getOptionalObject(json, "r")) { - _rotation = KeyframeGroup(rotationData.value()); - _rotationZInternalRepresentation = RotationZInternalRepresentation::R; - } - - // Opacity - if (const auto opacityData = getOptionalObject(json, "o")) { - _opacity = KeyframeGroup(opacityData.value()); - } - } catch(...) { - throw LottieParsingException(); - } - - _extraTy = getOptionalString(json, "ty"); - _extraSa = getOptionalAny(json, "sa"); - _extraSk = getOptionalAny(json, "sk"); - } - - lottiejson11::Json::object toJson() const { - lottiejson11::Json::object result; - - if (_anchorPoint.has_value()) { - result.insert(std::make_pair("a", _anchorPoint->toJson())); - } - - switch (_positionInternalRepresentation) { - case PositionInternalRepresentation::None: - assert(!_positionX.has_value()); - assert(!_positionY.has_value()); - assert(!_position.has_value()); - break; - case PositionInternalRepresentation::TopLevelXY: - assert(_positionX.has_value()); - assert(_positionY.has_value()); - assert(!_position.has_value()); - result.insert(std::make_pair("x", _positionX->toJson())); - result.insert(std::make_pair("y", _positionY->toJson())); - break; - case PositionInternalRepresentation::TopLevelCombined: - assert(_position.has_value()); - assert(!_positionX.has_value()); - assert(!_positionY.has_value()); - result.insert(std::make_pair("p", _position->toJson())); - break; - case PositionInternalRepresentation::NestedXY: - lottiejson11::Json::object nestedPosition; - assert(_positionX.has_value()); - assert(_positionY.has_value()); - assert(!_position.has_value()); - nestedPosition.insert(std::make_pair("x", _positionX->toJson())); - nestedPosition.insert(std::make_pair("y", _positionY->toJson())); - if (_extra_positionS.has_value()) { - nestedPosition.insert(std::make_pair("s", _extra_positionS.value())); - } - result.insert(std::make_pair("p", nestedPosition)); - break; - } - - if (_scale.has_value()) { - result.insert(std::make_pair("s", _scale->toJson())); - } - - if (_rotation.has_value()) { - switch (_rotationZInternalRepresentation) { - case RotationZInternalRepresentation::RZ: - result.insert(std::make_pair("rz", _rotation->toJson())); - break; - case RotationZInternalRepresentation::R: - result.insert(std::make_pair("r", _rotation->toJson())); - break; - } - } - - if (_opacity.has_value()) { - result.insert(std::make_pair("o", _opacity->toJson())); - } - - if (_extraTy.has_value()) { - result.insert(std::make_pair("ty", _extraTy.value())); - } - if (_extraSa.has_value()) { - result.insert(std::make_pair("sa", _extraSa.value())); - } - if (_extraSk.has_value()) { - result.insert(std::make_pair("sk", _extraSk.value())); - } - - return result; - } - - KeyframeGroup anchorPoint() { - if (_anchorPoint.has_value()) { - return _anchorPoint.value(); - } else { - return KeyframeGroup(Vector3D(0.0, 0.0, 0.0)); - } - } - - KeyframeGroup scale() const { - if (_scale) { - return _scale.value(); - } else { - return KeyframeGroup(Vector3D(100.0, 100.0, 100.0)); - } - } - - KeyframeGroup rotation() const { - if (_rotation) { - return _rotation.value(); - } else { - return KeyframeGroup(Vector1D(0.0)); - } - } - - KeyframeGroup opacity() const { - if (_opacity) { - return _opacity.value(); - } else { - return KeyframeGroup(Vector1D(100.0)); - } - } - - std::optional> const &position() const { - return _position; - } - - std::optional> const &positionX() const { - return _positionX; - } - - std::optional> const &positionY() const { - return _positionY; - } - -private: - /// The anchor point of the transform. - std::optional> _anchorPoint; - - /// The position of the transform. This is nil if the position data was split. - std::optional> _position; - - /// The positionX of the transform. This is nil if the position property is set. - std::optional> _positionX; - - /// The positionY of the transform. This is nil if the position property is set. - std::optional> _positionY; - - PositionInternalRepresentation _positionInternalRepresentation = PositionInternalRepresentation::None; - - /// The scale of the transform - std::optional> _scale; - - /// The rotation of the transform. Note: This is single dimensional rotation. - std::optional> _rotation; - - /// The opacity of the transform. - std::optional> _opacity; - - /// Should always be nil. - std::optional> _rotationZ; - RotationZInternalRepresentation _rotationZInternalRepresentation; - - std::optional _extra_positionS; - std::optional _extraTy; - std::optional _extraSa; - std::optional _extraSk; -}; - -} - -#endif /* Transform_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Ellipse.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Ellipse.cpp deleted file mode 100644 index 9378276f24f..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Ellipse.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "Ellipse.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Ellipse.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Ellipse.hpp deleted file mode 100644 index e1aff804dd4..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Ellipse.hpp +++ /dev/null @@ -1,64 +0,0 @@ -#ifndef Ellipse_hpp -#define Ellipse_hpp - -#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -namespace lottie { - -enum class PathDirection: int { - Clockwise = 1, - UserSetClockwise = 2, - CounterClockwise = 3 -}; - -/// An item that define an ellipse shape -class Ellipse: public ShapeItem { -public: - explicit Ellipse(lottiejson11::Json::object const &json) noexcept(false) : - ShapeItem(json), - position(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))), - size(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))) { - if (const auto directionRawValue = getOptionalInt(json, "d")) { - switch (directionRawValue.value()) { - case 1: - direction = PathDirection::Clockwise; - break; - case 2: - direction = PathDirection::UserSetClockwise; - break; - case 3: - direction = PathDirection::CounterClockwise; - break; - default: - throw LottieParsingException(); - } - } - - position = KeyframeGroup(getObject(json, "p")); - size = KeyframeGroup(getObject(json, "s")); - } - - virtual ~Ellipse() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - ShapeItem::toJson(json); - - if (direction.has_value()) { - json.insert(std::make_pair("d", (int)direction.value())); - } - json.insert(std::make_pair("p", position.toJson())); - json.insert(std::make_pair("s", size.toJson())); - } - -public: - std::optional direction; - KeyframeGroup position; - KeyframeGroup size; -}; - -} - -#endif /* Ellipse_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Fill.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Fill.cpp deleted file mode 100644 index 992a25a5be2..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Fill.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "Fill.hpp" - -namespace lottie { - -} - diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Fill.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Fill.hpp deleted file mode 100644 index f8139628a66..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Fill.hpp +++ /dev/null @@ -1,71 +0,0 @@ -#ifndef Fill_hpp -#define Fill_hpp - -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" -#import -#include -#include "Lottie/Private/Parsing/JsonParsing.hpp" -#include - -namespace lottie { - -class Fill: public ShapeItem { -public: - explicit Fill(lottiejson11::Json::object const &json) noexcept(false) : - ShapeItem(json), - opacity(KeyframeGroup(Vector1D(0.0))), - color(KeyframeGroup(Color(0.0, 0.0, 0.0, 0.0))) { - opacity = KeyframeGroup(getObject(json, "o")); - color = KeyframeGroup(getObject(json, "c")); - - if (const auto fillRuleRawValue = getOptionalInt(json, "r")) { - switch (fillRuleRawValue.value()) { - case 0: - fillRule = FillRule::None; - break; - case 1: - fillRule = FillRule::NonZeroWinding; - break; - case 2: - fillRule = FillRule::EvenOdd; - break; - default: - throw LottieParsingException(); - } - } - - fillEnabled = getOptionalBool(json, "fillEnabled"); - } - - virtual ~Fill() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - ShapeItem::toJson(json); - - json.insert(std::make_pair("o", opacity.toJson())); - json.insert(std::make_pair("c", color.toJson())); - - if (fillRule.has_value()) { - json.insert(std::make_pair("r", (int)fillRule.value())); - } - - if (fillEnabled.has_value()) { - json.insert(std::make_pair("fillEnabled", fillEnabled.value())); - } - } - -public: - KeyframeGroup opacity; - - /// The color keyframes for the fill - KeyframeGroup color; - - std::optional fillRule; - - std::optional fillEnabled; -}; - -} - -#endif /* Fill_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientFill.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientFill.cpp deleted file mode 100644 index f2ed96d1b9e..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientFill.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "GradientFill.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientFill.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientFill.hpp deleted file mode 100644 index c0fecb7bafb..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientFill.hpp +++ /dev/null @@ -1,113 +0,0 @@ -#ifndef GradientFill_hpp -#define GradientFill_hpp - -#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" -#include "Lottie/Public/Primitives/GradientColorSet.hpp" -#include - -namespace lottie { - -/// An item that define a gradient fill -class GradientFill: public ShapeItem { -public: - explicit GradientFill(lottiejson11::Json::object const &json) noexcept(false) : - ShapeItem(json), - opacity(KeyframeGroup(Vector1D(100.0))), - startPoint(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))), - endPoint(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))), - gradientType(GradientType::None), - numberOfColors(0), - colors(KeyframeGroup(GradientColorSet())) { - opacity = KeyframeGroup(getObject(json, "o")); - startPoint = KeyframeGroup(getObject(json, "s")); - endPoint = KeyframeGroup(getObject(json, "e")); - - auto gradientTypeRawValue = getInt(json, "t"); - switch (gradientTypeRawValue) { - case 0: - gradientType = GradientType::None; - break; - case 1: - gradientType = GradientType::Linear; - break; - case 2: - gradientType = GradientType::Radial; - break; - default: - throw LottieParsingException(); - } - - if (const auto highlightLengthData = getOptionalObject(json, "h")) { - highlightLength = KeyframeGroup(highlightLengthData.value()); - } - if (const auto highlightAngleData = getOptionalObject(json, "a")) { - highlightAngle = KeyframeGroup(highlightAngleData.value()); - } - - auto colorsContainer = getObject(json, "g"); - numberOfColors = getInt(colorsContainer, "p"); - colors = KeyframeGroup(getObject(colorsContainer, "k")); - - rValue = getOptionalInt(json, "r"); - } - - virtual ~GradientFill() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - ShapeItem::toJson(json); - - json.insert(std::make_pair("o", opacity.toJson())); - json.insert(std::make_pair("s", startPoint.toJson())); - json.insert(std::make_pair("e", endPoint.toJson())); - json.insert(std::make_pair("t", (int)gradientType)); - - if (highlightLength.has_value()) { - json.insert(std::make_pair("h", highlightLength->toJson())); - } - if (highlightAngle.has_value()) { - json.insert(std::make_pair("a", highlightAngle->toJson())); - } - - lottiejson11::Json::object colorsContainer; - colorsContainer.insert(std::make_pair("p", numberOfColors)); - colorsContainer.insert(std::make_pair("k", colors.toJson())); - json.insert(std::make_pair("g", colorsContainer)); - - if (rValue.has_value()) { - json.insert(std::make_pair("r", rValue.value())); - } - } - -public: - /// The opacity of the fill - KeyframeGroup opacity; - - /// The start of the gradient - KeyframeGroup startPoint; - - /// The end of the gradient - KeyframeGroup endPoint; - - /// The type of gradient - GradientType gradientType; - - /// Gradient Highlight Length. Only if type is Radial - std::optional> highlightLength; - - /// Highlight Angle. Only if type is Radial - std::optional> highlightAngle; - - /// The number of color points in the gradient - int numberOfColors; - - /// The Colors of the gradient. - KeyframeGroup colors; - - std::optional rValue; -}; - -} - -#endif /* GradientFill_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientStroke.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientStroke.cpp deleted file mode 100644 index 465b20fde88..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientStroke.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "GradientStroke.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientStroke.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientStroke.hpp deleted file mode 100644 index 95bed62b519..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/GradientStroke.hpp +++ /dev/null @@ -1,193 +0,0 @@ -#ifndef GradientStroke_hpp -#define GradientStroke_hpp - -#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" -#include "Lottie/Private/Model/ShapeItems/GradientFill.hpp" -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include "Lottie/Private/Model/Objects/DashElement.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" -#include - -namespace lottie { - -/// An item that define a gradient stroke -class GradientStroke: public ShapeItem { -public: - explicit GradientStroke(lottiejson11::Json::object const &json) noexcept(false) : - ShapeItem(json), - opacity(KeyframeGroup(Vector1D(100.0))), - startPoint(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))), - endPoint(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))), - gradientType(GradientType::None), - numberOfColors(0), - colors(KeyframeGroup(GradientColorSet())), - width(KeyframeGroup(Vector1D(0.0))), - lineCap(LineCap::Round), - lineJoin(LineJoin::Round) { - opacity = KeyframeGroup(getObject(json, "o")); - startPoint = KeyframeGroup(getObject(json, "s")); - endPoint = KeyframeGroup(getObject(json, "e")); - - auto gradientTypeRawValue = getInt(json, "t"); - switch (gradientTypeRawValue) { - case 0: - gradientType = GradientType::None; - break; - case 1: - gradientType = GradientType::Linear; - break; - case 2: - gradientType = GradientType::Radial; - break; - default: - throw LottieParsingException(); - } - - if (const auto highlightLengthData = getOptionalObject(json, "h")) { - highlightLength = KeyframeGroup(highlightLengthData.value()); - } - if (const auto highlightAngleData = getOptionalObject(json, "a")) { - highlightAngle = KeyframeGroup(highlightAngleData.value()); - } - - width = KeyframeGroup(getObject(json, "w")); - - if (const auto lineCapRawValue = getOptionalInt(json, "lc")) { - switch (lineCapRawValue.value()) { - case 0: - lineCap = LineCap::None; - break; - case 1: - lineCap = LineCap::Butt; - break; - case 2: - lineCap = LineCap::Round; - break; - case 3: - lineCap = LineCap::Square; - break; - default: - throw LottieParsingException(); - } - } - - if (const auto lineJoinRawValue = getOptionalInt(json, "lj")) { - switch (lineJoinRawValue.value()) { - case 0: - lineJoin = LineJoin::None; - break; - case 1: - lineJoin = LineJoin::Miter; - break; - case 2: - lineJoin = LineJoin::Round; - break; - case 3: - lineJoin = LineJoin::Bevel; - break; - default: - throw LottieParsingException(); - } - } - - if (const auto miterLimitData = getOptionalDouble(json, "ml")) { - miterLimit = (float)miterLimitData.value(); - } - - auto colorsContainer = getObject(json, "g"); - numberOfColors = getInt(colorsContainer, "p"); - auto colorsData = getObject(colorsContainer, "k"); - colors = KeyframeGroup(colorsData); - - if (const auto dashElementsData = getOptionalObjectArray(json, "d")) { - dashPattern = std::vector(); - for (const auto &dashElementData : dashElementsData.value()) { - dashPattern->push_back(DashElement(dashElementData)); - } - } - } - - virtual ~GradientStroke() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - ShapeItem::toJson(json); - - json.insert(std::make_pair("o", opacity.toJson())); - json.insert(std::make_pair("s", startPoint.toJson())); - json.insert(std::make_pair("e", endPoint.toJson())); - json.insert(std::make_pair("t", (int)gradientType)); - - if (highlightLength.has_value()) { - json.insert(std::make_pair("h", highlightLength->toJson())); - } - if (highlightAngle.has_value()) { - json.insert(std::make_pair("a", highlightAngle->toJson())); - } - - json.insert(std::make_pair("w", width.toJson())); - - json.insert(std::make_pair("lc", (int)lineCap)); - json.insert(std::make_pair("lj", (int)lineJoin)); - - if (miterLimit.has_value()) { - json.insert(std::make_pair("ml", miterLimit.value())); - } - - lottiejson11::Json::object colorsContainer; - colorsContainer.insert(std::make_pair("p", numberOfColors)); - colorsContainer.insert(std::make_pair("k", colors.toJson())); - json.insert(std::make_pair("g", colorsContainer)); - - if (dashPattern.has_value()) { - lottiejson11::Json::array dashElements; - for (const auto &dashElement : dashPattern.value()) { - dashElements.push_back(dashElement.toJson()); - } - json.insert(std::make_pair("d", dashElements)); - } - } - -public: - /// The opacity of the fill - KeyframeGroup opacity; - - /// The start of the gradient - KeyframeGroup startPoint; - - /// The end of the gradient - KeyframeGroup endPoint; - - /// The type of gradient - GradientType gradientType; - - /// Gradient Highlight Length. Only if type is Radial - std::optional> highlightLength; - - /// Highlight Angle. Only if type is Radial - std::optional> highlightAngle; - - /// The number of color points in the gradient - int numberOfColors; - - /// The Colors of the gradient. - KeyframeGroup colors; - - /// The width of the stroke - KeyframeGroup width; - - /// Line Cap - LineCap lineCap; - - /// Line Join - LineJoin lineJoin; - - /// Miter Limit - std::optional miterLimit; - - /// The dash pattern of the stroke - std::optional> dashPattern; -}; - -} - -#endif /* GradientStroke_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Group.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Group.cpp deleted file mode 100644 index 89ea7daea73..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Group.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "Group.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Group.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Group.hpp deleted file mode 100644 index 7bc2eb4d10b..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Group.hpp +++ /dev/null @@ -1,53 +0,0 @@ -#ifndef Group_hpp -#define Group_hpp - -#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include -#include - -namespace lottie { - -/// An item that define an ellipse shape -class Group: public ShapeItem { -public: - explicit Group(lottiejson11::Json::object const &json) noexcept(false) : - ShapeItem(json) { - auto itemsData = getObjectArray(json, "it"); - for (const auto &itemData : itemsData) { - items.push_back(parseShapeItem(itemData)); - } - - numberOfProperties = getOptionalInt(json, "np"); - } - - virtual ~Group() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - ShapeItem::toJson(json); - - lottiejson11::Json::array itemArray; - for (const auto &item : items) { - lottiejson11::Json::object itemJson; - item->toJson(itemJson); - itemArray.push_back(itemJson); - } - - json.insert(std::make_pair("it", itemArray)); - - if (numberOfProperties.has_value()) { - json.insert(std::make_pair("np", numberOfProperties.value())); - } - } - -public: - /// A list of shape items. - std::vector> items; - - std::optional numberOfProperties; -}; - -} - -#endif /* Group_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Merge.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Merge.cpp deleted file mode 100644 index b22b05d1f6d..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Merge.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "Merge.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Merge.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Merge.hpp deleted file mode 100644 index 4f6fcc48818..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Merge.hpp +++ /dev/null @@ -1,64 +0,0 @@ -#ifndef Merge_hpp -#define Merge_hpp - -#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -namespace lottie { - -enum class MergeMode: int { - None = 0, - Merge = 1, - Add = 2, - Subtract = 3, - Intersect = 4, - Exclude = 5 -}; - -/// An item that define an ellipse shape -class Merge: public ShapeItem { -public: - explicit Merge(lottiejson11::Json::object const &json) noexcept(false) : - ShapeItem(json), - mode(MergeMode::None) { - auto modeRawValue = getInt(json, "mm"); - switch (modeRawValue) { - case 0: - mode = MergeMode::None; - break; - case 1: - mode = MergeMode::Merge; - break; - case 2: - mode = MergeMode::Add; - break; - case 3: - mode = MergeMode::Subtract; - break; - case 4: - mode = MergeMode::Intersect; - break; - case 5: - mode = MergeMode::Exclude; - break; - default: - throw LottieParsingException(); - } - } - - virtual ~Merge() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - ShapeItem::toJson(json); - - json.insert(std::make_pair("mm", (int)mode)); - } - -public: - /// The mode of the merge path - MergeMode mode; -}; - -} - -#endif /* Merge_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Rectangle.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Rectangle.cpp deleted file mode 100644 index c686130aa9e..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Rectangle.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "Rectangle.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Rectangle.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Rectangle.hpp deleted file mode 100644 index 67926f2e291..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Rectangle.hpp +++ /dev/null @@ -1,101 +0,0 @@ -#ifndef Rectangle_hpp -#define Rectangle_hpp - -#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" -#include "Lottie/Private/Model/ShapeItems/Ellipse.hpp" -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -namespace lottie { - -/// An item that define an ellipse shape -class Rectangle: public ShapeItem { -public: - explicit Rectangle(lottiejson11::Json::object const &json) noexcept(false) : - ShapeItem(json), - position(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))), - size(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))), - cornerRadius(KeyframeGroup(Vector1D(0.0))) { - if (const auto directionRawValue = getOptionalInt(json, "d")) { - switch (directionRawValue.value()) { - case 1: - direction = PathDirection::Clockwise; - break; - case 2: - direction = PathDirection::UserSetClockwise; - break; - case 3: - direction = PathDirection::CounterClockwise; - break; - default: - throw LottieParsingException(); - } - } - - position = KeyframeGroup(getObject(json, "p")); - size = KeyframeGroup(getObject(json, "s")); - cornerRadius = KeyframeGroup(getObject(json, "r")); - } - - virtual ~Rectangle() = default; - - explicit Rectangle( - std::optional name_, - std::optional matchName_, - std::optional expressionIndex_, - std::optional cix_, - std::optional _hidden_, - std::optional index_, - std::optional blendMode_, - std::optional layerClass_, - std::optional direction_, - KeyframeGroup position_, - KeyframeGroup size_, - KeyframeGroup cornerRadius_ - ) : - ShapeItem( - name_, - matchName_, - expressionIndex_, - cix_, - ShapeType::Rectangle, - _hidden_, - index_, - blendMode_, - layerClass_ - ), - direction(direction_), - position(position_), - size(size_), - cornerRadius(cornerRadius_) { - } - - virtual void toJson(lottiejson11::Json::object &json) const override { - ShapeItem::toJson(json); - - if (direction.has_value()) { - json.insert(std::make_pair("d", (int)direction.value())); - } - - json.insert(std::make_pair("p", position.toJson())); - json.insert(std::make_pair("s", size.toJson())); - json.insert(std::make_pair("r", cornerRadius.toJson())); - } - -public: - /// The direction of the rect. - std::optional direction; - - /// The position - KeyframeGroup position; - - /// The size - KeyframeGroup size; - - /// The Corner radius of the rectangle - KeyframeGroup cornerRadius; -}; - -} - -#endif /* Rectangle_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Repeater.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Repeater.cpp deleted file mode 100644 index 70477e31af8..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Repeater.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "Repeater.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Repeater.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Repeater.hpp deleted file mode 100644 index 8bb25a1c375..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Repeater.hpp +++ /dev/null @@ -1,100 +0,0 @@ -#ifndef Repeater_hpp -#define Repeater_hpp - -#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -namespace lottie { - -/// An item that define a repeater -class Repeater: public ShapeItem { -public: - explicit Repeater(lottiejson11::Json::object const &json) noexcept(false) : - ShapeItem(json) { - if (const auto copiesData = getOptionalObject(json, "c")) { - copies = KeyframeGroup(copiesData.value()); - } - if (const auto offsetData = getOptionalObject(json, "o")) { - offset = KeyframeGroup(offsetData.value()); - } - - auto transformContainer = getObject(json, "tr"); - if (const auto startOpacityData = getOptionalObject(transformContainer, "so")) { - startOpacity = KeyframeGroup(startOpacityData.value()); - } - if (const auto endOpacityData = getOptionalObject(transformContainer, "eo")) { - endOpacity = KeyframeGroup(endOpacityData.value()); - } - if (const auto rotationData = getOptionalObject(transformContainer, "r")) { - rotation = KeyframeGroup(rotationData.value()); - } - if (const auto positionData = getOptionalObject(transformContainer, "p")) { - position = KeyframeGroup(positionData.value()); - } - if (const auto scaleData = getOptionalObject(transformContainer, "s")) { - scale = KeyframeGroup(scaleData.value()); - } - } - - virtual ~Repeater() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - ShapeItem::toJson(json); - - if (copies.has_value()) { - json.insert(std::make_pair("c", copies->toJson())); - } - if (offset.has_value()) { - json.insert(std::make_pair("o", offset->toJson())); - } - - lottiejson11::Json::object transformContainer; - if (startOpacity.has_value()) { - json.insert(std::make_pair("so", startOpacity->toJson())); - } - if (endOpacity.has_value()) { - json.insert(std::make_pair("eo", endOpacity->toJson())); - } - if (rotation.has_value()) { - json.insert(std::make_pair("r", rotation->toJson())); - } - if (position.has_value()) { - json.insert(std::make_pair("p", position->toJson())); - } - if (scale.has_value()) { - json.insert(std::make_pair("s", scale->toJson())); - } - - json.insert(std::make_pair("tr", transformContainer)); - } - -public: - /// The number of copies to repeat - std::optional> copies; - - /// The offset of each copy - std::optional> offset; - - /// Start Opacity - std::optional> startOpacity; - - /// End opacity - std::optional> endOpacity; - - /// The rotation - std::optional> rotation; - - /// Anchor Point - std::optional> anchorPoint; - - /// Position - std::optional> position; - - /// Scale - std::optional> scale; -}; - -} - -#endif /* Repeater_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/RoundedRectangle.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/RoundedRectangle.cpp deleted file mode 100644 index 64cba093630..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/RoundedRectangle.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "RoundedRectangle.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/RoundedRectangle.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/RoundedRectangle.hpp deleted file mode 100644 index 548d23a32d8..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/RoundedRectangle.hpp +++ /dev/null @@ -1,77 +0,0 @@ -#ifndef RoundedRectangle_hpp -#define RoundedRectangle_hpp - -#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" -#include "Lottie/Private/Model/ShapeItems/Ellipse.hpp" -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -namespace lottie { - -/// An item that define an ellipse shape -class RoundedRectangle: public ShapeItem { -public: - explicit RoundedRectangle(lottiejson11::Json::object const &json) noexcept(false) : - ShapeItem(json), - cornerRadius(KeyframeGroup(Vector1D(0.0))) { - if (const auto directionRawValue = getOptionalInt(json, "d")) { - switch (directionRawValue.value()) { - case 1: - direction = PathDirection::Clockwise; - break; - case 2: - direction = PathDirection::UserSetClockwise; - break; - case 3: - direction = PathDirection::CounterClockwise; - break; - default: - throw LottieParsingException(); - } - } - - if (const auto positionData = getOptionalObject(json, "p")) { - position = KeyframeGroup(positionData.value()); - } - if (const auto sizeData = getOptionalObject(json, "s")) { - size = KeyframeGroup(sizeData.value()); - } - - cornerRadius = KeyframeGroup(getObject(json, "r")); - } - - virtual ~RoundedRectangle() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - ShapeItem::toJson(json); - - if (direction.has_value()) { - json.insert(std::make_pair("d", (int)direction.value())); - } - if (position.has_value()) { - json.insert(std::make_pair("p", position->toJson())); - } - if (size.has_value()) { - json.insert(std::make_pair("s", size->toJson())); - } - - json.insert(std::make_pair("r", cornerRadius.toJson())); - } - -public: - /// The direction of the rect. - std::optional direction; - - /// The position - std::optional> position; - - /// The size - std::optional> size; - - /// The Corner radius of the rectangle - KeyframeGroup cornerRadius; -}; - -} - -#endif /* RoundedRectangle_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Shape.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Shape.cpp deleted file mode 100644 index bf5a96876c4..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Shape.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "Shape.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Shape.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Shape.hpp deleted file mode 100644 index 66a5a36f85d..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Shape.hpp +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef Shape_hpp -#define Shape_hpp - -#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" -#include "Lottie/Private/Model/ShapeItems/Ellipse.hpp" -#include -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include - -namespace lottie { - -/// An item that defines an custom shape -class Shape: public ShapeItem { -public: - explicit Shape(lottiejson11::Json::object const &json) noexcept(false) : - ShapeItem(json), - path(KeyframeGroup(getObject(json, "ks"))) { - if (const auto directionRawValue = getOptionalInt(json, "d")) { - switch (directionRawValue.value()) { - case 1: - direction = PathDirection::Clockwise; - break; - case 2: - direction = PathDirection::UserSetClockwise; - break; - case 3: - direction = PathDirection::CounterClockwise; - break; - default: - throw LottieParsingException(); - } - } - } - - virtual ~Shape() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - ShapeItem::toJson(json); - - json.insert(std::make_pair("ks", path.toJson())); - - if (direction.has_value()) { - json.insert(std::make_pair("d", (int)direction.value())); - } - } - -public: - KeyframeGroup path; - std::optional direction; -}; - -} - -#endif /* Shape_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeItem.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeItem.cpp deleted file mode 100644 index a47b348c29c..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeItem.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include "ShapeItem.hpp" - -#include "Ellipse.hpp" -#include "Fill.hpp" -#include "GradientFill.hpp" -#include "Group.hpp" -#include "GradientStroke.hpp" -#include "Merge.hpp" -#include "Rectangle.hpp" -#include "RoundedRectangle.hpp" -#include "Repeater.hpp" -#include "Shape.hpp" -#include "Star.hpp" -#include "Stroke.hpp" -#include "Trim.hpp" -#include "ShapeTransform.hpp" - -namespace lottie { - -std::shared_ptr parseShapeItem(lottiejson11::Json::object const &json) noexcept(false) { - auto typeRawValue = getString(json, "ty"); - if (typeRawValue == "el") { - return std::make_shared(json); - } else if (typeRawValue == "fl") { - return std::make_shared(json); - } else if (typeRawValue == "gf") { - return std::make_shared(json); - } else if (typeRawValue == "gr") { - return std::make_shared(json); - } else if (typeRawValue == "gs") { - return std::make_shared(json); - } else if (typeRawValue == "mm") { - return std::make_shared(json); - } else if (typeRawValue == "rc") { - return std::make_shared(json); - } else if (typeRawValue == "rp") { - return std::make_shared(json); - } else if (typeRawValue == "sh") { - return std::make_shared(json); - } else if (typeRawValue == "sr") { - return std::make_shared(json); - } else if (typeRawValue == "st") { - return std::make_shared(json); - } else if (typeRawValue == "tm") { - return std::make_shared(json); - } else if (typeRawValue == "tr") { - return std::make_shared(json); - } else if (typeRawValue == "rd") { - return std::make_shared(json); - } else { - throw LottieParsingException(); - } -} - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeItem.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeItem.hpp deleted file mode 100644 index 892eba1db2c..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeItem.hpp +++ /dev/null @@ -1,209 +0,0 @@ -#ifndef ShapeItem_hpp -#define ShapeItem_hpp - -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include - -namespace lottie { - -enum class ShapeType { - Ellipse, - Fill, - GradientFill, - Group, - GradientStroke, - Merge, - Rectangle, - Repeater, - Shape, - Star, - Stroke, - Trim, - Transform, - RoundedRectangle -}; - -/// An item belonging to a Shape Layer -class ShapeItem { -public: - ShapeItem(lottiejson11::Json const &jsonAny) noexcept(false) : - type(ShapeType::Ellipse) { - if (!jsonAny.is_object()) { - throw LottieParsingException(); - } - - lottiejson11::Json::object const &json = jsonAny.object_items(); - - name = getOptionalString(json, "nm"); - matchName = getOptionalString(json, "mn"); - expressionIndex = getOptionalInt(json, "ix"); - cix = getOptionalInt(json, "cix"); - - index = getOptionalInt(json, "ind"); - blendMode = getOptionalInt(json, "bm"); - - auto typeRawValue = getString(json, "ty"); - if (typeRawValue == "el") { - type = ShapeType::Ellipse; - } else if (typeRawValue == "fl") { - type = ShapeType::Fill; - } else if (typeRawValue == "gf") { - type = ShapeType::GradientFill; - } else if (typeRawValue == "gr") { - type = ShapeType::Group; - } else if (typeRawValue == "gs") { - type = ShapeType::GradientStroke; - } else if (typeRawValue == "mm") { - type = ShapeType::Merge; - } else if (typeRawValue == "rc") { - type = ShapeType::Rectangle; - } else if (typeRawValue == "rp") { - type = ShapeType::Repeater; - } else if (typeRawValue == "sh") { - type = ShapeType::Shape; - } else if (typeRawValue == "sr") { - type = ShapeType::Star; - } else if (typeRawValue == "st") { - type = ShapeType::Stroke; - } else if (typeRawValue == "tm") { - type = ShapeType::Trim; - } else if (typeRawValue == "tr") { - type = ShapeType::Transform; - } else if (typeRawValue == "rd") { - type = ShapeType::RoundedRectangle; - } else { - throw LottieParsingException(); - } - - _hidden = getOptionalBool(json, "hd"); - - layerClass = getOptionalString(json, "cl"); - } - - ShapeItem( - std::optional name_, - std::optional matchName_, - std::optional expressionIndex_, - std::optional cix_, - ShapeType type_, - std::optional _hidden_, - std::optional index_, - std::optional blendMode_, - std::optional layerClass_ - ) : - name(name_), - matchName(matchName_), - expressionIndex(expressionIndex_), - cix(cix_), - type(type_), - _hidden(_hidden_), - index(index_), - blendMode(blendMode_), - layerClass(layerClass_) { - } - - ShapeItem(const ShapeItem&) = delete; - ShapeItem& operator=(ShapeItem&) = delete; - - virtual void toJson(lottiejson11::Json::object &json) const { - if (name.has_value()) { - json.insert(std::make_pair("nm", name.value())); - } - if (matchName.has_value()) { - json.insert(std::make_pair("mn", matchName.value())); - } - if (expressionIndex.has_value()) { - json.insert(std::make_pair("ix", expressionIndex.value())); - } - if (cix.has_value()) { - json.insert(std::make_pair("cix", cix.value())); - } - - if (index.has_value()) { - json.insert(std::make_pair("ind", index.value())); - } - if (blendMode.has_value()) { - json.insert(std::make_pair("bm", blendMode.value())); - } - - switch (type) { - case ShapeType::Ellipse: - json.insert(std::make_pair("ty", "el")); - break; - case ShapeType::Fill: - json.insert(std::make_pair("ty", "fl")); - break; - case ShapeType::GradientFill: - json.insert(std::make_pair("ty", "gf")); - break; - case ShapeType::Group: - json.insert(std::make_pair("ty", "gr")); - break; - case ShapeType::GradientStroke: - json.insert(std::make_pair("ty", "gs")); - break; - case ShapeType::Merge: - json.insert(std::make_pair("ty", "mm")); - break; - case ShapeType::Rectangle: - json.insert(std::make_pair("ty", "rc")); - break; - case ShapeType::RoundedRectangle: - json.insert(std::make_pair("ty", "rd")); - break; - case ShapeType::Repeater: - json.insert(std::make_pair("ty", "rp")); - break; - case ShapeType::Shape: - json.insert(std::make_pair("ty", "sh")); - break; - case ShapeType::Star: - json.insert(std::make_pair("ty", "sr")); - break; - case ShapeType::Stroke: - json.insert(std::make_pair("ty", "st")); - break; - case ShapeType::Trim: - json.insert(std::make_pair("ty", "tm")); - break; - case ShapeType::Transform: - json.insert(std::make_pair("ty", "tr")); - break; - } - - if (_hidden.has_value()) { - json.insert(std::make_pair("hd", _hidden.value())); - } - - if (layerClass.has_value()) { - json.insert(std::make_pair("cl", layerClass.value())); - } - } - - bool hidden() const { - if (_hidden.has_value()) { - return _hidden.value(); - } else { - return false; - } - } - -public: - std::optional name; - std::optional matchName; - std::optional expressionIndex; - std::optional cix; - ShapeType type; - std::optional _hidden; - std::optional index; - std::optional blendMode; - - std::optional layerClass; -}; - -std::shared_ptr parseShapeItem(lottiejson11::Json::object const &json) noexcept(false); - -} - -#endif /* ShapeItem_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeTransform.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeTransform.cpp deleted file mode 100644 index 1df07218bc0..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeTransform.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "ShapeTransform.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeTransform.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeTransform.hpp deleted file mode 100644 index fcfd28a2b8f..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/ShapeTransform.hpp +++ /dev/null @@ -1,91 +0,0 @@ -#ifndef ShapeTransform_hpp -#define ShapeTransform_hpp - -#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -namespace lottie { - -/// An item that define a shape transform -class ShapeTransform: public ShapeItem { -public: - explicit ShapeTransform(lottiejson11::Json::object const &json) noexcept(false) : - ShapeItem(json) { - if (const auto anchorData = getOptionalObject(json, "a")) { - anchor = KeyframeGroup(anchorData.value()); - } - if (const auto positionData = getOptionalObject(json, "p")) { - position = KeyframeGroup(positionData.value()); - } - if (const auto scaleData = getOptionalObject(json, "s")) { - scale = KeyframeGroup(scaleData.value()); - } - if (const auto rotationData = getOptionalObject(json, "r")) { - rotation = KeyframeGroup(rotationData.value()); - } - if (const auto opacityData = getOptionalObject(json, "o")) { - opacity = KeyframeGroup(opacityData.value()); - } - if (const auto skewData = getOptionalObject(json, "sk")) { - skew = KeyframeGroup(skewData.value()); - } - if (const auto skewAxisData = getOptionalObject(json, "sa")) { - skewAxis = KeyframeGroup(skewAxisData.value()); - } - } - - virtual ~ShapeTransform() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - ShapeItem::toJson(json); - - if (anchor.has_value()) { - json.insert(std::make_pair("a", anchor->toJson())); - } - if (position.has_value()) { - json.insert(std::make_pair("p", position->toJson())); - } - if (scale.has_value()) { - json.insert(std::make_pair("s", scale->toJson())); - } - if (rotation.has_value()) { - json.insert(std::make_pair("r", rotation->toJson())); - } - if (opacity.has_value()) { - json.insert(std::make_pair("o", opacity->toJson())); - } - if (skew.has_value()) { - json.insert(std::make_pair("sk", skew->toJson())); - } - if (skewAxis.has_value()) { - json.insert(std::make_pair("sa", skewAxis->toJson())); - } - } - -public: - /// Anchor Point - std::optional> anchor; - - /// Position - std::optional> position; - - /// Scale - std::optional> scale; - - /// Rotation - std::optional> rotation; - - /// opacity - std::optional> opacity; - - /// Skew - std::optional> skew; - - /// Skew Axis - std::optional> skewAxis; -}; - -} - -#endif /* ShapeTransform_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Star.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Star.cpp deleted file mode 100644 index c5f38f5946d..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Star.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "Star.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Star.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Star.hpp deleted file mode 100644 index c9f711526a6..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Star.hpp +++ /dev/null @@ -1,133 +0,0 @@ -#ifndef Star_hpp -#define Star_hpp - -#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" -#include "Lottie/Private/Model/ShapeItems/Ellipse.hpp" -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include - -namespace lottie { - -enum class StarType: int { - None = 0, - Star = 1, - Polygon = 2 -}; - -/// An item that define a star shape -class Star: public ShapeItem { -public: - explicit Star(lottiejson11::Json::object const &json) noexcept(false) : - ShapeItem(json), - position(KeyframeGroup(Vector3D(0.0, 0.0, 0.0))), - outerRadius(KeyframeGroup(Vector1D(0.0))), - outerRoundness(KeyframeGroup(Vector1D(0.0))), - rotation(KeyframeGroup(Vector1D(0.0))), - points(KeyframeGroup(Vector1D(0.0))), - starType(StarType::None) { - if (const auto directionRawValue = getOptionalInt(json, "d")) { - switch (directionRawValue.value()) { - case 1: - direction = PathDirection::Clockwise; - break; - case 2: - direction = PathDirection::UserSetClockwise; - break; - case 3: - direction = PathDirection::CounterClockwise; - break; - default: - throw LottieParsingException(); - } - } - - position = KeyframeGroup(getObject(json, "p")); - outerRadius = KeyframeGroup(getObject(json, "or")); - outerRoundness = KeyframeGroup(getObject(json, "os")); - - if (const auto innerRadiusData = getOptionalObject(json, "ir")) { - innerRadius = KeyframeGroup(innerRadiusData.value()); - } - if (const auto innerRoundnessData = getOptionalObject(json, "is")) { - innerRoundness = KeyframeGroup(innerRoundnessData.value()); - } - - rotation = KeyframeGroup(getObject(json, "r")); - points = KeyframeGroup(getObject(json, "pt")); - - auto starTypeRawValue = getInt(json, "sy"); - switch (starTypeRawValue) { - case 0: - starType = StarType::None; - break; - case 1: - starType = StarType::Star; - break; - case 2: - starType = StarType::Polygon; - break; - default: - throw LottieParsingException(); - } - } - - virtual ~Star() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - ShapeItem::toJson(json); - - if (direction.has_value()) { - json.insert(std::make_pair("d", (int)direction.value())); - } - - json.insert(std::make_pair("p", position.toJson())); - json.insert(std::make_pair("or", outerRadius.toJson())); - json.insert(std::make_pair("os", outerRoundness.toJson())); - - if (innerRadius.has_value()) { - json.insert(std::make_pair("ir", innerRadius->toJson())); - } - if (innerRoundness.has_value()) { - json.insert(std::make_pair("is", innerRoundness->toJson())); - } - - json.insert(std::make_pair("r", rotation.toJson())); - json.insert(std::make_pair("pt", points.toJson())); - - json.insert(std::make_pair("sy", (int)starType)); - } - -public: - /// The direction of the star. - std::optional direction; - - /// The position of the star - KeyframeGroup position; - - /// The outer radius of the star - KeyframeGroup outerRadius; - - /// The outer roundness of the star - KeyframeGroup outerRoundness; - - /// The outer radius of the star - std::optional> innerRadius; - - /// The outer roundness of the star - std::optional> innerRoundness; - - /// The rotation of the star - KeyframeGroup rotation; - - /// The number of points on the star - KeyframeGroup points; - - /// The type of star - StarType starType; -}; - -} - -#endif /* Star_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Stroke.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Stroke.cpp deleted file mode 100644 index 0c858f24425..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Stroke.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "Stroke.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Stroke.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Stroke.hpp deleted file mode 100644 index e306fcd6a6a..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Stroke.hpp +++ /dev/null @@ -1,142 +0,0 @@ -#ifndef Stroke_hpp -#define Stroke_hpp - -#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" -#include "Lottie/Private/Model/ShapeItems/GradientStroke.hpp" -#import -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include "Lottie/Private/Model/Objects/DashElement.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include - -namespace lottie { - -/// An item that define an ellipse shape -class Stroke: public ShapeItem { -public: - explicit Stroke(lottiejson11::Json::object const &json) noexcept(false) : - ShapeItem(json), - opacity(KeyframeGroup(Vector1D(100.0))), - color(KeyframeGroup(Color(0.0, 0.0, 0.0, 0.0))), - width(KeyframeGroup(Vector1D(0.0))), - lineCap(LineCap::Round), - lineJoin(LineJoin::Round) { - opacity = KeyframeGroup(getObject(json, "o")); - color = KeyframeGroup(getObject(json, "c")); - width = KeyframeGroup(getObject(json, "w")); - - if (const auto lineCapRawValue = getOptionalInt(json, "lc")) { - switch (lineCapRawValue.value()) { - case 0: - lineCap = LineCap::None; - break; - case 1: - lineCap = LineCap::Butt; - break; - case 2: - lineCap = LineCap::Round; - break; - case 3: - lineCap = LineCap::Square; - break; - default: - throw LottieParsingException(); - } - } - - if (const auto lineJoinRawValue = getOptionalInt(json, "lj")) { - switch (lineJoinRawValue.value()) { - case 0: - lineJoin = LineJoin::None; - break; - case 1: - lineJoin = LineJoin::Miter; - break; - case 2: - lineJoin = LineJoin::Round; - break; - case 3: - lineJoin = LineJoin::Bevel; - break; - default: - throw LottieParsingException(); - } - } - - if (const auto miterLimitData = getOptionalDouble(json, "ml")) { - miterLimit = (float)miterLimitData.value(); - } - - if (const auto dashElementsData = getOptionalObjectArray(json, "d")) { - dashPattern = std::vector(); - for (const auto &dashElementData : dashElementsData.value()) { - dashPattern->push_back(DashElement(dashElementData)); - } - } - - fillEnabled = getOptionalBool(json, "fillEnabled"); - ml2 = getOptionalAny(json, "ml2"); - } - - virtual ~Stroke() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - ShapeItem::toJson(json); - - json.insert(std::make_pair("o", opacity.toJson())); - json.insert(std::make_pair("c", color.toJson())); - json.insert(std::make_pair("w", width.toJson())); - - json.insert(std::make_pair("lc", (int)lineCap)); - json.insert(std::make_pair("lj", (int)lineJoin)); - - if (miterLimit.has_value()) { - json.insert(std::make_pair("ml", miterLimit.value())); - } - - if (dashPattern.has_value()) { - lottiejson11::Json::array dashElements; - for (const auto &dashElement : dashPattern.value()) { - dashElements.push_back(dashElement.toJson()); - } - json.insert(std::make_pair("d", dashElements)); - } - - if (fillEnabled.has_value()) { - json.insert(std::make_pair("fillEnabled", fillEnabled.value())); - } - if (ml2.has_value()) { - json.insert(std::make_pair("ml2", ml2.value())); - } - } - -public: - /// The opacity of the stroke - KeyframeGroup opacity; - - /// The Color of the stroke - KeyframeGroup color; - - /// The width of the stroke - KeyframeGroup width; - - /// Line Cap - LineCap lineCap; - - /// Line Join - LineJoin lineJoin; - - /// Miter Limit - std::optional miterLimit; - - /// The dash pattern of the stroke - std::optional> dashPattern; - - std::optional fillEnabled; - std::optional ml2; -}; - -} - -#endif /* Stroke_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.cpp deleted file mode 100644 index 85f95d8b0cf..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "Trim.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.hpp deleted file mode 100644 index e4628d604a1..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.hpp +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef Trim_hpp -#define Trim_hpp - -#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -namespace lottie { - -/// An item that defines trim -class Trim: public ShapeItem { -public: - explicit Trim(lottiejson11::Json::object const &json) noexcept(false) : - ShapeItem(json), - start(KeyframeGroup(Vector1D(0.0))), - end(KeyframeGroup(Vector1D(0.0))), - offset(KeyframeGroup(Vector1D(0.0))), - trimType(TrimType::Simultaneously) { - start = KeyframeGroup(getObject(json, "s")); - end = KeyframeGroup(getObject(json, "e")); - offset = KeyframeGroup(getObject(json, "o")); - - auto trimTypeRawValue = getInt(json, "m"); - switch (trimTypeRawValue) { - case 1: - trimType = TrimType::Simultaneously; - break; - case 2: - trimType = TrimType::Individually; - break; - default: - throw LottieParsingException(); - } - } - - virtual ~Trim() = default; - - virtual void toJson(lottiejson11::Json::object &json) const override { - ShapeItem::toJson(json); - - json.insert(std::make_pair("s", start.toJson())); - json.insert(std::make_pair("e", end.toJson())); - json.insert(std::make_pair("o", offset.toJson())); - json.insert(std::make_pair("m", (int)trimType)); - } - -public: - KeyframeGroup start; - KeyframeGroup end; - KeyframeGroup offset; - TrimType trimType; -}; - -} - -#endif /* Trim_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Font.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Font.cpp deleted file mode 100644 index 7dfd91b3cc3..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Font.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "Font.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Font.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Font.hpp deleted file mode 100644 index b321983e714..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Font.hpp +++ /dev/null @@ -1,103 +0,0 @@ -#ifndef Font_hpp -#define Font_hpp - -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include -#include - -namespace lottie { - -class Font { -public: - Font( - std::string const &name_, - std::string const &familyName_, - std::string const &style_, - float ascent_ - ) : - name(name_), - familyName(familyName_), - style(style_), - ascent(ascent_) { - } - - explicit Font(lottiejson11::Json::object const &json) noexcept(false) { - name = getString(json, "fName"); - familyName = getString(json, "fFamily"); - path = getOptionalString(json, "fPath"); - weight = getOptionalString(json, "fWeight"); - fontClass = getOptionalString(json, "fClass"); - style = getString(json, "fStyle"); - ascent = (float)getDouble(json, "ascent"); - origin = getOptionalInt(json, "origin"); - } - - lottiejson11::Json::object toJson() const { - lottiejson11::Json::object result; - - result.insert(std::make_pair("fName", name)); - result.insert(std::make_pair("fFamily", familyName)); - if (path.has_value()) { - result.insert(std::make_pair("fPath", path.value())); - } - if (weight.has_value()) { - result.insert(std::make_pair("fWeight", weight.value())); - } - if (fontClass.has_value()) { - result.insert(std::make_pair("fClass", fontClass.value())); - } - result.insert(std::make_pair("fStyle", style)); - result.insert(std::make_pair("ascent", ascent)); - if (origin.has_value()) { - result.insert(std::make_pair("origin", origin.value())); - } - - return result; - } - -public: - std::string name; - std::string familyName; - std::optional path; - std::optional weight; - std::optional fontClass; - std::string style; - float ascent; - std::optional origin; -}; - -/// A list of fonts -class FontList { -public: - FontList(std::vector const &fonts_) : - fonts(fonts_) { - } - - explicit FontList(lottiejson11::Json::object const &json) noexcept(false) { - if (const auto fontsData = getOptionalObjectArray(json, "list")) { - for (const auto &fontData : fontsData.value()) { - fonts.emplace_back(fontData); - } - } - } - - lottiejson11::Json::object toJson() const { - lottiejson11::Json::array fontArray; - - for (const auto &font : fonts) { - fontArray.push_back(font.toJson()); - } - - lottiejson11::Json::object result; - result.insert(std::make_pair("list", fontArray)); - return result; - } - -public: - std::vector fonts; -}; - -} - -#endif /* Font_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Glyph.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Glyph.cpp deleted file mode 100644 index 071050db7a7..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Glyph.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "Glyph.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Glyph.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Glyph.hpp deleted file mode 100644 index d5c15228507..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/Glyph.hpp +++ /dev/null @@ -1,108 +0,0 @@ -#ifndef Glyph_hpp -#define Glyph_hpp - -#include "Lottie/Private/Model/ShapeItems/ShapeItem.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include -#include - -namespace lottie { - -class Glyph { -public: - Glyph( - std::string const &character_, - float fontSize_, - std::string const &fontFamily_, - std::string const &fontStyle_, - float width_, - std::optional>> shapes_ - ) : - character(character_), - fontSize(fontSize_), - fontFamily(fontFamily_), - fontStyle(fontStyle_), - width(width_), - shapes(shapes_) { - } - - explicit Glyph(lottiejson11::Json::object const &json) noexcept(false) : - character(""), - fontSize(0.0), - fontFamily(""), - fontStyle(""), - width(0.0) { - character = getString(json, "ch"); - fontSize = (float)getDouble(json, "size"); - fontFamily = getString(json, "fFamily"); - fontStyle = getString(json, "style"); - width = (float)getDouble(json, "w"); - - if (const auto shapeContainer = getOptionalObject(json, "data")) { - internalHasData = true; - - if (const auto shapesData = getOptionalObjectArray(shapeContainer.value(), "shapes")) { - shapes = std::vector>(); - - for (const auto &shapeData : shapesData.value()) { - shapes->push_back(parseShapeItem(shapeData)); - } - } - } - } - - lottiejson11::Json::object toJson() const { - lottiejson11::Json::object result; - - result.insert(std::make_pair("ch", character)); - result.insert(std::make_pair("size", fontSize)); - result.insert(std::make_pair("fFamily", fontFamily)); - result.insert(std::make_pair("style", fontStyle)); - result.insert(std::make_pair("w", width)); - - if (internalHasData || shapes.has_value()) { - lottiejson11::Json::object shapeContainer; - - if (shapes.has_value()) { - lottiejson11::Json::array shapeArray; - - for (const auto &shape : shapes.value()) { - lottiejson11::Json::object shapeJson; - shape->toJson(shapeJson); - shapeArray.push_back(shapeJson); - } - - shapeContainer.insert(std::make_pair("shapes", shapeArray)); - } - result.insert(std::make_pair("data", shapeContainer)); - } - - return result; - } - -public: - /// The character - std::string character; - - /// The font size of the character - float fontSize; - - /// The font family of the character - std::string fontFamily; - - /// The Style of the character - std::string fontStyle; - - /// The Width of the character - float width; - - /// The Shape Data of the Character - std::optional>> shapes; - - bool internalHasData = false; -}; - -} - -#endif /* Glyph_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextAnimator.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextAnimator.cpp deleted file mode 100644 index 7cd9b257158..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextAnimator.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "TextAnimator.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextAnimator.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextAnimator.hpp deleted file mode 100644 index 6724c7e3fae..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextAnimator.hpp +++ /dev/null @@ -1,183 +0,0 @@ -#ifndef TextAnimator_hpp -#define TextAnimator_hpp - -#include -#import -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include -#include - -namespace lottie { - -class TextAnimator { -public: - TextAnimator( - std::optional &name_, - std::optional> anchor_, - std::optional> position_, - std::optional> scale_, - std::optional> skew_, - std::optional> skewAxis_, - std::optional> rotation_, - std::optional> opacity_, - std::optional> strokeColor_, - std::optional> fillColor_, - std::optional> strokeWidth_, - std::optional> tracking_ - ) : - name(name_), - anchor(anchor_), - position(position_), - scale(scale_), - skew(skew_), - skewAxis(skewAxis_), - rotation(rotation_), - opacity(opacity_), - strokeColor(strokeColor_), - fillColor(fillColor_), - strokeWidth(strokeWidth_), - tracking(tracking_) { - } - - explicit TextAnimator(lottiejson11::Json const &jsonAny) { - if (!jsonAny.is_object()) { - throw LottieParsingException(); - } - lottiejson11::Json::object const &json = jsonAny.object_items(); - - if (const auto nameData = getOptionalString(json, "nm")) { - name = nameData.value(); - } - _extraS = getOptionalAny(json, "s"); - - lottiejson11::Json::object const &animatorContainer = getObject(json, "a"); - - if (const auto fillColorData = getOptionalObject(animatorContainer, "fc")) { - fillColor = KeyframeGroup(fillColorData.value()); - } - if (const auto strokeColorData = getOptionalObject(animatorContainer, "sc")) { - strokeColor = KeyframeGroup(strokeColorData.value()); - } - if (const auto strokeWidthData = getOptionalObject(animatorContainer, "sw")) { - strokeWidth = KeyframeGroup(strokeWidthData.value()); - } - if (const auto trackingData = getOptionalObject(animatorContainer, "t")) { - tracking = KeyframeGroup(trackingData.value()); - } - if (const auto anchorData = getOptionalObject(animatorContainer, "a")) { - anchor = KeyframeGroup(anchorData.value()); - } - if (const auto positionData = getOptionalObject(animatorContainer, "p")) { - position = KeyframeGroup(positionData.value()); - } - if (const auto scaleData = getOptionalObject(animatorContainer, "s")) { - scale = KeyframeGroup(scaleData.value()); - } - if (const auto skewData = getOptionalObject(animatorContainer, "sk")) { - skew = KeyframeGroup(skewData.value()); - } - if (const auto skewAxisData = getOptionalObject(animatorContainer, "sa")) { - skewAxis = KeyframeGroup(skewAxisData.value()); - } - if (const auto rotationData = getOptionalObject(animatorContainer, "r")) { - rotation = KeyframeGroup(rotationData.value()); - } - if (const auto opacityData = getOptionalObject(animatorContainer, "o")) { - opacity = KeyframeGroup(opacityData.value()); - } - } - - lottiejson11::Json::object toJson() const { - lottiejson11::Json::object animatorContainer; - - if (fillColor.has_value()) { - animatorContainer.insert(std::make_pair("fc", fillColor->toJson())); - } - if (strokeColor.has_value()) { - animatorContainer.insert(std::make_pair("sc", strokeColor->toJson())); - } - if (strokeWidth.has_value()) { - animatorContainer.insert(std::make_pair("sw", strokeWidth->toJson())); - } - if (tracking.has_value()) { - animatorContainer.insert(std::make_pair("t", tracking->toJson())); - } - if (anchor.has_value()) { - animatorContainer.insert(std::make_pair("a", anchor->toJson())); - } - if (position.has_value()) { - animatorContainer.insert(std::make_pair("p", position->toJson())); - } - if (scale.has_value()) { - animatorContainer.insert(std::make_pair("s", scale->toJson())); - } - if (skew.has_value()) { - animatorContainer.insert(std::make_pair("sk", skew->toJson())); - } - if (skewAxis.has_value()) { - animatorContainer.insert(std::make_pair("sa", skewAxis->toJson())); - } - if (rotation.has_value()) { - animatorContainer.insert(std::make_pair("r", rotation->toJson())); - } - if (opacity.has_value()) { - animatorContainer.insert(std::make_pair("o", opacity->toJson())); - } - - lottiejson11::Json::object result; - result.insert(std::make_pair("a", animatorContainer)); - - if (name.has_value()) { - result.insert(std::make_pair("nm", name.value())); - } - if (_extraS.has_value()) { - result.insert(std::make_pair("s", _extraS.value())); - } - - return result; - } - -public: - std::optional name; - - /// Anchor - std::optional> anchor; - - /// Position - std::optional> position; - - /// Scale - std::optional> scale; - - /// Skew - std::optional> skew; - - /// Skew Axis - std::optional> skewAxis; - - /// Rotation - std::optional> rotation; - - /// Opacity - std::optional> opacity; - - /// Stroke Color - std::optional> strokeColor; - - /// Fill Color - std::optional> fillColor; - - /// Stroke Width - std::optional> strokeWidth; - - /// Tracking - std::optional> tracking; - - std::optional _extraS; -}; - -} - -#endif /* TextAnimator_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextDocument.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextDocument.cpp deleted file mode 100644 index 95c3be2b676..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextDocument.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "TextDocument.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextDocument.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextDocument.hpp deleted file mode 100644 index 83ea39ebcc0..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/Text/TextDocument.hpp +++ /dev/null @@ -1,190 +0,0 @@ -#ifndef TextDocument_hpp -#define TextDocument_hpp - -#include -#import -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include -#include - -namespace lottie { - -enum class TextJustification: int { - Left = 0, - Right = 1, - Center = 2 -}; - -class TextDocument { -public: - TextDocument( - std::string const &text_, - float fontSize_, - std::string const &fontFamily_, - TextJustification justification_, - int tracking_, - float lineHeight_, - std::optional baseline_, - std::optional fillColorData_, - std::optional strokeColorData_, - std::optional strokeWidth_, - std::optional strokeOverFill_, - std::optional textFramePosition_, - std::optional textFrameSize_ - ) : - text(text_), - fontSize(fontSize_), - fontFamily(fontFamily_), - justification(justification_), - tracking(tracking_), - lineHeight(lineHeight_), - baseline(baseline_), - fillColorData(fillColorData_), - strokeColorData(strokeColorData_), - strokeWidth(strokeWidth_), - strokeOverFill(strokeOverFill_), - textFramePosition(textFramePosition_), - textFrameSize(textFrameSize_) { - } - - explicit TextDocument(lottiejson11::Json const &jsonAny) noexcept(false) : - text(""), - fontSize(0.0), - fontFamily(""), - justification(TextJustification::Left), - tracking(0), - lineHeight(0.0) { - if (!jsonAny.is_object()) { - throw LottieParsingException(); - } - - lottiejson11::Json::object const &json = jsonAny.object_items(); - - text = getString(json, "t"); - fontSize = (float)getDouble(json, "s"); - fontFamily = getString(json, "f"); - - auto justificationRawValue = getInt(json, "j"); - switch (justificationRawValue) { - case 0: - justification = TextJustification::Left; - break; - case 1: - justification = TextJustification::Right; - break; - case 2: - justification = TextJustification::Center; - break; - default: - throw LottieParsingException(); - } - - tracking = getInt(json, "tr"); - lineHeight = (float)getDouble(json, "lh"); - if (const auto baselineValue = getOptionalDouble(json, "ls")) { - baseline = (float)baselineValue.value(); - } - - if (const auto fillColorDataValue = getOptionalAny(json, "fc")) { - fillColorData = Color(fillColorDataValue.value()); - } - - if (const auto strokeColorDataValue = getOptionalAny(json, "sc")) { - strokeColorData = Color(strokeColorDataValue.value()); - } - - if (const auto strokeWidthValue = getOptionalDouble(json, "sw")) { - strokeWidth = (float)strokeWidthValue.value(); - } - if (const auto strokeOverFillValue = getOptionalBool(json, "of")) { - strokeOverFill = (float)strokeOverFillValue.value(); - } - - if (const auto textFramePositionData = getOptionalAny(json, "ps")) { - textFramePosition = Vector3D(textFramePositionData.value()); - } - if (const auto textFrameSizeData = getOptionalAny(json, "sz")) { - textFrameSize = Vector3D(textFrameSizeData.value()); - } - } - - lottiejson11::Json::object toJson() const { - lottiejson11::Json::object result; - - result.insert(std::make_pair("t", text)); - result.insert(std::make_pair("s", fontSize)); - result.insert(std::make_pair("f", fontFamily)); - result.insert(std::make_pair("j", (int)justification)); - result.insert(std::make_pair("tr", tracking)); - result.insert(std::make_pair("lh", lineHeight)); - - if (baseline.has_value()) { - result.insert(std::make_pair("ls", baseline.value())); - } - - if (fillColorData.has_value()) { - result.insert(std::make_pair("fc", fillColorData->toJson())); - } - if (strokeColorData.has_value()) { - result.insert(std::make_pair("sc", strokeColorData->toJson())); - } - - if (strokeWidth.has_value()) { - result.insert(std::make_pair("sw", strokeWidth.value())); - } - if (strokeOverFill.has_value()) { - result.insert(std::make_pair("of", strokeOverFill.value())); - } - if (textFramePosition.has_value()) { - result.insert(std::make_pair("ps", textFramePosition->toJson())); - } - if (textFrameSize.has_value()) { - result.insert(std::make_pair("sz", textFrameSize->toJson())); - } - - return result; - } - -public: - /// The Text - std::string text; - - /// The Font size - float fontSize; - - /// The Font Family - std::string fontFamily; - - /// Justification - TextJustification justification; - - /// Tracking - int tracking; - - /// Line Height - float lineHeight; - - /// Baseline - std::optional baseline; - - /// Fill Color data - std::optional fillColorData; - - /// Scroke Color data - std::optional strokeColorData; - - /// Stroke Width - std::optional strokeWidth; - - /// Stroke Over Fill - std::optional strokeOverFill; - - std::optional textFramePosition; - - std::optional textFrameSize; -}; - -} - -#endif /* TextDocument_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.cpp deleted file mode 100644 index f72968be599..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.cpp +++ /dev/null @@ -1,219 +0,0 @@ -#include "JsonParsing.hpp" - -#include - -namespace lottie { - -thread_local int isExceptionExpectedLevel = 0; - -LottieParsingException::Guard::Guard() { - assert(isExceptionExpectedLevel >= 0); - isExceptionExpectedLevel++; -} - -LottieParsingException::Guard::~Guard() { - assert(isExceptionExpectedLevel - 1 >= 0); - isExceptionExpectedLevel--; -} - -LottieParsingException::LottieParsingException() { - if (isExceptionExpectedLevel == 0) { - assert(true); - } -} - -const char* LottieParsingException::what() const throw() { - return "Lottie parsing exception"; -} - -lottiejson11::Json getAny(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) { - auto value = object.find(key); - if (value == object.end()) { - throw LottieParsingException(); - } - return value->second; -} - -std::optional getOptionalAny(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) { - auto value = object.find(key); - if (value == object.end()) { - return std::nullopt; - } - return value->second; -} - -lottiejson11::Json::object getObject(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) { - auto value = object.find(key); - if (value == object.end()) { - throw LottieParsingException(); - } - if (!value->second.is_object()) { - throw LottieParsingException(); - } - return value->second.object_items(); -} - -std::optional getOptionalObject(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) { - auto value = object.find(key); - if (value == object.end()) { - return std::nullopt; - } - if (!value->second.is_object()) { - throw LottieParsingException(); - } - return value->second.object_items(); -} - -std::vector getObjectArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) { - auto value = object.find(key); - if (value == object.end()) { - throw LottieParsingException(); - } - if (!value->second.is_array()) { - throw LottieParsingException(); - } - - std::vector result; - for (const auto &item : value->second.array_items()) { - if (!item.is_object()) { - throw LottieParsingException(); - } - result.push_back(item.object_items()); - } - - return result; -} - -std::optional> getOptionalObjectArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) { - auto value = object.find(key); - if (value == object.end()) { - return std::nullopt; - } - if (!value->second.is_array()) { - throw LottieParsingException(); - } - - std::vector result; - for (const auto &item : value->second.array_items()) { - if (!item.is_object()) { - throw LottieParsingException(); - } - result.push_back(item.object_items()); - } - - return result; -} - -std::vector getAnyArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) { - auto value = object.find(key); - if (value == object.end()) { - throw LottieParsingException(); - } - if (!value->second.is_array()) { - throw LottieParsingException(); - } - - return value->second.array_items(); -} - -std::optional> getOptionalAnyArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) { - auto value = object.find(key); - if (value == object.end()) { - throw std::nullopt; - } - if (!value->second.is_array()) { - throw LottieParsingException(); - } - - return value->second.array_items(); -} - -std::string getString(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) { - auto value = object.find(key); - if (value == object.end()) { - throw LottieParsingException(); - } - if (!value->second.is_string()) { - throw LottieParsingException(); - } - return value->second.string_value(); -} - -std::optional getOptionalString(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) { - auto value = object.find(key); - if (value == object.end()) { - return std::nullopt; - } - if (!value->second.is_string()) { - throw LottieParsingException(); - } - return value->second.string_value(); -} - -int32_t getInt(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) { - auto value = object.find(key); - if (value == object.end()) { - throw LottieParsingException(); - } - if (!value->second.is_number()) { - throw LottieParsingException(); - } - return value->second.int_value(); -} - -std::optional getOptionalInt(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) { - auto value = object.find(key); - if (value == object.end()) { - return std::nullopt; - } - if (!value->second.is_number()) { - throw LottieParsingException(); - } - return value->second.int_value(); -} - -double getDouble(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) { - auto value = object.find(key); - if (value == object.end()) { - throw LottieParsingException(); - } - if (!value->second.is_number()) { - throw LottieParsingException(); - } - return value->second.number_value(); -} - -std::optional getOptionalDouble(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) { - auto value = object.find(key); - if (value == object.end()) { - return std::nullopt; - } - if (!value->second.is_number()) { - throw LottieParsingException(); - } - return value->second.number_value(); -} - -bool getBool(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) { - auto value = object.find(key); - if (value == object.end()) { - throw LottieParsingException(); - } - if (!value->second.is_bool()) { - throw LottieParsingException(); - } - return value->second.bool_value(); -} - -std::optional getOptionalBool(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) { - auto value = object.find(key); - if (value == object.end()) { - return std::nullopt; - } - if (!value->second.is_bool()) { - throw LottieParsingException(); - } - return value->second.bool_value(); -} - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.hpp deleted file mode 100644 index a0bd7951b3e..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Parsing/JsonParsing.hpp +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef JsonParsing_hpp -#define JsonParsing_hpp - -#include - -#include -#include -#include - -namespace lottie { - -class LottieParsingException: public std::exception { -public: - class Guard { - public: - Guard(); - ~Guard(); - }; - -public: - LottieParsingException(); - - virtual const char* what() const throw(); -}; - -lottiejson11::Json getAny(lottiejson11::Json::object const &object, std::string const &key) noexcept(false); -std::optional getOptionalAny(lottiejson11::Json::object const &object, std::string const &key) noexcept(false); - -lottiejson11::Json::object getObject(lottiejson11::Json::object const &object, std::string const &key) noexcept(false); -std::optional getOptionalObject(lottiejson11::Json::object const &object, std::string const &key) noexcept(false); - -std::vector getObjectArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false); -std::optional> getOptionalObjectArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false); - -std::vector getAnyArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false); -std::optional> getOptionalAnyArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false); - -std::string getString(lottiejson11::Json::object const &object, std::string const &key) noexcept(false); -std::optional getOptionalString(lottiejson11::Json::object const &object, std::string const &key) noexcept(false); - -int32_t getInt(lottiejson11::Json::object const &object, std::string const &key) noexcept(false); -std::optional getOptionalInt(lottiejson11::Json::object const &object, std::string const &key) noexcept(false); - -double getDouble(lottiejson11::Json::object const &object, std::string const &key) noexcept(false); -std::optional getOptionalDouble(lottiejson11::Json::object const &object, std::string const &key) noexcept(false); - -bool getBool(lottiejson11::Json::object const &object, std::string const &key) noexcept(false); -std::optional getOptionalBool(lottiejson11::Json::object const &object, std::string const &key) noexcept(false); - -} - -#endif /* JsonParsing_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/BezierPath.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/BezierPath.cpp deleted file mode 100644 index 85c50dba1e0..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/BezierPath.cpp +++ /dev/null @@ -1,690 +0,0 @@ -#include - -#include -#include - -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -namespace lottie { - -BezierTrimPathPosition::BezierTrimPathPosition(float start_, float end_) : -start(start_), -end(end_) { -} - -BezierPathContents::BezierPathContents(CurveVertex const &startPoint) : -elements({ PathElement(startPoint) }) { -} - -BezierPathContents::BezierPathContents() : -elements({}), -closed(false) { -} - -BezierPathContents::BezierPathContents(lottiejson11::Json const &jsonAny) noexcept(false) : -elements({}) { - lottiejson11::Json::object const *json = nullptr; - if (jsonAny.is_object()) { - json = &jsonAny.object_items(); - } else if (jsonAny.is_array()) { - if (jsonAny.array_items().empty()) { - throw LottieParsingException(); - } - if (!jsonAny.array_items()[0].is_object()) { - throw LottieParsingException(); - } - json = &jsonAny.array_items()[0].object_items(); - } - - if (const auto closedData = getOptionalBool(*json, "c")) { - closed = closedData.value(); - } - - auto vertexContainer = getAnyArray(*json, "v"); - auto inPointsContainer = getAnyArray(*json, "i"); - auto outPointsContainer = getAnyArray(*json, "o"); - - if (vertexContainer.size() != inPointsContainer.size() || inPointsContainer.size() != outPointsContainer.size()) { - throw LottieParsingException(); - } - if (vertexContainer.empty()) { - return; - } - - /// Create first point - Vector2D firstPoint = Vector2D(vertexContainer[0]); - Vector2D firstInPoint = Vector2D(inPointsContainer[0]); - Vector2D firstOutPoint = Vector2D(outPointsContainer[0]); - CurveVertex firstVertex = CurveVertex::relative( - firstPoint, - firstInPoint, - firstOutPoint - ); - PathElement previousElement(firstVertex); - elements.push_back(previousElement); - - for (size_t i = 1; i < vertexContainer.size(); i++) { - Vector2D point = Vector2D(vertexContainer[i]); - Vector2D inPoint = Vector2D(inPointsContainer[i]); - Vector2D outPoint = Vector2D(outPointsContainer[i]); - CurveVertex vertex = CurveVertex::relative( - point, - inPoint, - outPoint - ); - auto pathElement = previousElement.pathElementTo(vertex); - elements.push_back(pathElement); - previousElement = pathElement; - } - - if (closed.value_or(false)) { - auto closeElement = previousElement.pathElementTo(firstVertex); - elements.push_back(closeElement); - } -} - -lottiejson11::Json BezierPathContents::toJson() const { - lottiejson11::Json::object result; - - lottiejson11::Json::array vertices; - lottiejson11::Json::array inPoints; - lottiejson11::Json::array outPoints; - - for (const auto &element : elements) { - vertices.push_back(element.vertex.point.toJson()); - inPoints.push_back(element.vertex.inTangentRelative().toJson()); - outPoints.push_back(element.vertex.outTangentRelative().toJson()); - } - - result.insert(std::make_pair("v", vertices)); - result.insert(std::make_pair("i", inPoints)); - result.insert(std::make_pair("o", outPoints)); - - if (closed.has_value()) { - result.insert(std::make_pair("c", closed.value())); - } - - return lottiejson11::Json(result); -} - -std::shared_ptr BezierPathContents::cgPath() const { - auto cgPath = CGPath::makePath(); - - std::optional previousElement; - for (const auto &element : elements) { - if (previousElement.has_value()) { - if (previousElement->vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) { - cgPath->addLineTo(element.vertex.point); - } else { - cgPath->addCurveTo(element.vertex.point, previousElement->vertex.outTangent, element.vertex.inTangent); - } - } else { - cgPath->moveTo(element.vertex.point); - } - previousElement = element; - } - if (closed.value_or(true)) { - cgPath->closeSubpath(); - } - return cgPath; -} - -float BezierPathContents::length() { - if (_length.has_value()) { - return _length.value(); - } else { - float result = 0.0; - for (size_t i = 1; i < elements.size(); i++) { - result += elements[i].length(elements[i - 1]); - } - _length = result; - return result; - } -} - -void BezierPathContents::moveToStartPoint(CurveVertex const &vertex) { - elements = { PathElement(vertex) }; - _length = std::nullopt; -} - -void BezierPathContents::addVertex(CurveVertex const &vertex) { - addElement(PathElement(vertex)); -} - -void BezierPathContents::reserveCapacity(size_t capacity) { - elements.reserve(capacity); -} - -void BezierPathContents::setElementCount(size_t count) { - elements.resize(count, PathElement(CurveVertex::absolute(Vector2D(0.0, 0.0), Vector2D(0.0, 0.0), Vector2D(0.0, 0.0)))); -} - -void BezierPathContents::invalidateLength() { - _length.reset(); -} - -void BezierPathContents::addCurve(Vector2D const &toPoint, Vector2D const &outTangent, Vector2D const &inTangent) { - if (elements.empty()) { - return; - } - auto previous = elements[elements.size() - 1]; - auto newVertex = CurveVertex::absolute(toPoint, inTangent, toPoint); - updateVertex( - CurveVertex::absolute(previous.vertex.point, previous.vertex.inTangent, outTangent), - (int)elements.size() - 1, - false - ); - addVertex(newVertex); -} - -void BezierPathContents::addLine(Vector2D const &toPoint) { - if (elements.empty()) { - return; - } - auto previous = elements[elements.size() - 1]; - auto newVertex = CurveVertex::relative(toPoint, Vector2D::Zero(), Vector2D::Zero()); - updateVertex( - CurveVertex::absolute(previous.vertex.point, previous.vertex.inTangent, previous.vertex.point), - (int)elements.size() - 1, - false - ); - addVertex(newVertex); -} - -void BezierPathContents::close() { - closed = true; -} - -void BezierPathContents::addElement(PathElement const &pathElement) { - elements.push_back(pathElement); -} - -void BezierPathContents::updateVertex(CurveVertex const &vertex, int atIndex, bool remeasure) { - if (remeasure) { - PathElement newElement(CurveVertex::absolute(Vector2D::Zero(), Vector2D::Zero(), Vector2D::Zero())); - if (atIndex > 0) { - auto previousElement = elements[atIndex - 1]; - newElement = previousElement.pathElementTo(vertex); - } else { - newElement = PathElement(vertex); - } - elements[atIndex] = newElement; - - if (atIndex + 1 < elements.size()) { - auto nextElement = elements[atIndex + 1]; - elements[atIndex + 1] = newElement.pathElementTo(nextElement.vertex); - } - - } else { - auto oldElement = elements[atIndex]; - elements[atIndex] = oldElement.updateVertex(vertex); - } -} - -std::vector> BezierPathContents::trim(float fromLength, float toLength, float offsetLength) { - if (elements.size() <= 1) { - return {}; - } - - if (fromLength == toLength) { - return {}; - } - - float lengthValue = length(); - - /// Normalize lengths to the curve length. - auto start = fmod(fromLength + offsetLength, lengthValue); - auto end = fmod(toLength + offsetLength, lengthValue); - - if (start < 0.0) { - start = lengthValue + start; - } - - if (end < 0.0) { - end = lengthValue + end; - } - - if (start == lengthValue) { - start = 0.0; - } - if (end == 0.0) { - end = lengthValue; - } - - if ( - (start == 0.0 && end == lengthValue) || - start == end || - (start == lengthValue && end == 0.0) - ) { - /// The trim encompasses the entire path. Return. - return { shared_from_this() }; - } - - if (start > end) { - // Start is greater than end. Two paths are returned. - return trimPathAtLengths({ - BezierTrimPathPosition(0.0, end), - BezierTrimPathPosition(start, lengthValue) - }); - } - - return trimPathAtLengths({ BezierTrimPathPosition(start, end) }); -} - -// MARK: Private - -std::vector> BezierPathContents::trimPathAtLengths(std::vector const &positions) { - if (positions.empty()) { - return {}; - } - auto remainingPositions = positions; - - auto trim = remainingPositions[0]; - remainingPositions.erase(remainingPositions.begin()); - - std::vector> paths; - - float runningLength = 0.0; - bool finishedTrimming = false; - auto pathElements = elements; - - auto currentPath = std::make_shared(); - int i = 0; - - while (!finishedTrimming) { - if (pathElements.size() <= i) { - /// Do this for rounding errors - paths.push_back(currentPath); - finishedTrimming = true; - continue; - } - /// Loop through and add elements within start->end range. - /// Get current element - auto element = pathElements[i]; - float elementLength = 0.0; - if (i != 0) { - elementLength = element.length(pathElements[i - 1]); - } - - /// Calculate new running length. - auto newLength = runningLength + elementLength; - - if (newLength < trim.start) { - /// Element is not included in the trim, continue. - runningLength = newLength; - i = i + 1; - /// Increment index, we are done with this element. - continue; - } - - if (newLength == trim.start) { - /// Current element IS the start element. - /// For start we want to add a zero length element. - currentPath->moveToStartPoint(element.vertex); - runningLength = newLength; - i = i + 1; - /// Increment index, we are done with this element. - continue; - } - - if (runningLength < trim.start && trim.start < newLength && currentPath->elements.size() == 0) { - /// The start of the trim is between this element and the previous, trim. - /// Get previous element. - auto previousElement = pathElements[i - 1]; - /// Trim it - auto trimLength = trim.start - runningLength; - auto trimResults = element.splitElementAtPosition(previousElement, trimLength); - /// Add the right span start. - currentPath->moveToStartPoint(trimResults.rightSpan.start.vertex); - - pathElements[i] = trimResults.rightSpan.end; - pathElements[i - 1] = trimResults.rightSpan.start; - runningLength = runningLength + trimResults.leftSpan.end.length(trimResults.leftSpan.start); - /// Dont increment index or the current length, the end of this path can be within this span. - continue; - } - - if (trim.start < newLength && newLength < trim.end) { - /// Element lies within the trim span. - currentPath->addElement(element); - runningLength = newLength; - i = i + 1; - continue; - } - - if (newLength == trim.end) { - /// Element is the end element. - /// The element could have a new length if it's added right after the start node. - currentPath->addElement(element); - /// We are done with this span. - runningLength = newLength; - i = i + 1; - /// Allow the path to be finalized. - /// Fall through to finalize path and move to next position - } - - if (runningLength < trim.end && trim.end < newLength) { - /// New element must be cut for end. - /// Get previous element. - auto previousElement = pathElements[i - 1]; - /// Trim it - auto trimLength = trim.end - runningLength; - auto trimResults = element.splitElementAtPosition(previousElement, trimLength); - /// Add the left span end. - - currentPath->updateVertex(trimResults.leftSpan.start.vertex, (int)currentPath->elements.size() - 1, false); - currentPath->addElement(trimResults.leftSpan.end); - - pathElements[i] = trimResults.rightSpan.end; - pathElements[i - 1] = trimResults.rightSpan.start; - runningLength = runningLength + trimResults.leftSpan.end.length(trimResults.leftSpan.start); - /// Dont increment index or the current length, the start of the next path can be within this span. - /// We are done with this span. - /// Allow the path to be finalized. - /// Fall through to finalize path and move to next position - } - - paths.push_back(currentPath); - currentPath = std::make_shared(); - if (remainingPositions.size() > 0) { - trim = remainingPositions[0]; - remainingPositions.erase(remainingPositions.begin()); - } else { - finishedTrimming = true; - } - } - return paths; -} - -BezierPath::BezierPath(CurveVertex const &startPoint) : -_contents(std::make_shared(startPoint)) { -} - -BezierPath::BezierPath() : -_contents(std::make_shared()) { -} - -BezierPath::BezierPath(lottiejson11::Json const &jsonAny) noexcept(false) : -_contents(std::make_shared(jsonAny)) { -} - -lottiejson11::Json BezierPath::toJson() const { - return _contents->toJson(); -} - -float BezierPath::length() { - return _contents->length(); -} - -void BezierPath::moveToStartPoint(CurveVertex const &vertex) { - _contents->moveToStartPoint(vertex); -} - -void BezierPath::addVertex(CurveVertex const &vertex) { - _contents->addVertex(vertex); -} - -void BezierPath::reserveCapacity(size_t capacity) { - _contents->reserveCapacity(capacity); -} - -void BezierPath::setElementCount(size_t count) { - _contents->setElementCount(count); -} - -void BezierPath::invalidateLength() { - _contents->invalidateLength(); -} - -void BezierPath::addCurve(Vector2D const &toPoint, Vector2D const &outTangent, Vector2D const &inTangent) { - _contents->addCurve(toPoint, outTangent, inTangent); -} - -void BezierPath::addLine(Vector2D const &toPoint) { - _contents->addLine(toPoint); -} - -void BezierPath::close() { - _contents->close(); -} - -void BezierPath::addElement(PathElement const &pathElement) { - _contents->addElement(pathElement); -} - -void BezierPath::updateVertex(CurveVertex const &vertex, int atIndex, bool remeasure) { - _contents->updateVertex(vertex, atIndex, remeasure); -} - -std::vector BezierPath::trim(float fromLength, float toLength, float offsetLength) { - std::vector result; - - auto resultContents = _contents->trim(fromLength, toLength, offsetLength); - for (const auto &resultContent : resultContents) { - result.emplace_back(resultContent); - } - - return result; -} - -std::vector const &BezierPath::elements() const { - return _contents->elements; -} - -std::vector &BezierPath::mutableElements() { - return _contents->elements; -} - -std::optional const &BezierPath::closed() const { - return _contents->closed; -} -void BezierPath::setClosed(std::optional const &closed) { - _contents->closed = closed; -} - -std::shared_ptr BezierPath::cgPath() const { - return _contents->cgPath(); -} - -BezierPath BezierPath::copyUsingTransform(Transform2D const &transform) const { - if (transform == Transform2D::identity()) { - return (*this); - } - BezierPath result; - result._contents->closed = _contents->closed; - result.reserveCapacity(_contents->elements.size()); - for (const auto &element : _contents->elements) { - result._contents->elements.emplace_back(element.vertex.transformed(transform)); - } - return result; -} - -BezierPath::BezierPath(std::shared_ptr contents) : -_contents(contents) { -} - -BezierPathsBoundingBoxContext::BezierPathsBoundingBoxContext() : -pointsX((float *)malloc(1024 * 4)), -pointsY((float *)malloc(1024 * 4)), -pointsSize(1024) { -} - -BezierPathsBoundingBoxContext::~BezierPathsBoundingBoxContext() { - free(pointsX); - free(pointsY); -} - -static CGRect calculateBoundingRectOpt(float const *pointsX, float const *pointsY, int count) { - float minX = 0.0; - float maxX = 0.0; - vDSP_minv(pointsX, 1, &minX, count); - vDSP_maxv(pointsX, 1, &maxX, count); - - float minY = 0.0; - float maxY = 0.0; - vDSP_minv(pointsY, 1, &minY, count); - vDSP_maxv(pointsY, 1, &maxY, count); - - return CGRect(minX, minY, maxX - minX, maxY - minY); -} - -CGRect bezierPathsBoundingBoxParallel(BezierPathsBoundingBoxContext &context, std::vector const &paths) { - int pointCount = 0; - - float *pointsX = context.pointsX; - float *pointsY = context.pointsY; - int pointsSize = context.pointsSize; - - for (const auto &path : paths) { - PathElement const *pathElements = path.elements().data(); - int pathElementCount = (int)path.elements().size(); - - for (int i = 0; i < pathElementCount; i++) { - const auto &element = pathElements[i]; - - if (pointsSize < pointCount + 1) { - pointsSize = (pointCount + 1) * 2; - pointsX = (float *)realloc(pointsX, pointsSize * 4); - pointsY = (float *)realloc(pointsY, pointsSize * 4); - } - pointsX[pointCount] = (float)element.vertex.point.x; - pointsY[pointCount] = (float)element.vertex.point.y; - pointCount++; - - if (i != 0) { - const auto &previousElement = pathElements[i - 1]; - if (previousElement.vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) { - } else { - if (pointsSize < pointCount + 1) { - pointsSize = (pointCount + 2) * 2; - pointsX = (float *)realloc(pointsX, pointsSize * 4); - pointsY = (float *)realloc(pointsY, pointsSize * 4); - } - pointsX[pointCount] = (float)previousElement.vertex.outTangent.x; - pointsY[pointCount] = (float)previousElement.vertex.outTangent.y; - pointCount++; - pointsX[pointCount] = (float)element.vertex.inTangent.x; - pointsY[pointCount] = (float)element.vertex.inTangent.y; - pointCount++; - } - } - } - } - - context.pointsX = pointsX; - context.pointsY = pointsY; - context.pointsSize = pointsSize; - - if (pointCount == 0) { - return CGRect(0.0, 0.0, 0.0, 0.0); - } - - return calculateBoundingRectOpt(pointsX, pointsY, pointCount); -} - -CGRect bezierPathsBoundingBoxParallel(BezierPathsBoundingBoxContext &context, BezierPath const &path) { - int pointCount = 0; - - float *pointsX = context.pointsX; - float *pointsY = context.pointsY; - int pointsSize = context.pointsSize; - - PathElement const *pathElements = path.elements().data(); - int pathElementCount = (int)path.elements().size(); - - for (int i = 0; i < pathElementCount; i++) { - const auto &element = pathElements[i]; - - if (pointsSize < pointCount + 1) { - pointsSize = (pointCount + 1) * 2; - pointsX = (float *)realloc(pointsX, pointsSize * 4); - pointsY = (float *)realloc(pointsY, pointsSize * 4); - } - pointsX[pointCount] = (float)element.vertex.point.x; - pointsY[pointCount] = (float)element.vertex.point.y; - pointCount++; - - if (i != 0) { - const auto &previousElement = pathElements[i - 1]; - if (previousElement.vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) { - } else { - if (pointsSize < pointCount + 1) { - pointsSize = (pointCount + 2) * 2; - pointsX = (float *)realloc(pointsX, pointsSize * 4); - pointsY = (float *)realloc(pointsY, pointsSize * 4); - } - pointsX[pointCount] = (float)previousElement.vertex.outTangent.x; - pointsY[pointCount] = (float)previousElement.vertex.outTangent.y; - pointCount++; - pointsX[pointCount] = (float)element.vertex.inTangent.x; - pointsY[pointCount] = (float)element.vertex.inTangent.y; - pointCount++; - } - } - } - - context.pointsX = pointsX; - context.pointsY = pointsY; - context.pointsSize = pointsSize; - - if (pointCount == 0) { - return CGRect(0.0, 0.0, 0.0, 0.0); - } - - return calculateBoundingRectOpt(pointsX, pointsY, pointCount); -} - -CGRect bezierPathsBoundingBox(std::vector const &paths) { - int pointCount = 0; - - float *pointsX = (float *)malloc(128 * 4); - float *pointsY = (float *)malloc(128 * 4); - int pointsSize = 128; - - for (const auto &path : paths) { - PathElement const *pathElements = path.elements().data(); - int pathElementCount = (int)path.elements().size(); - - for (int i = 0; i < pathElementCount; i++) { - const auto &element = pathElements[i]; - - if (pointsSize < pointCount + 1) { - pointsSize = (pointCount + 1) * 2; - pointsX = (float *)realloc(pointsX, pointsSize * 4); - pointsY = (float *)realloc(pointsY, pointsSize * 4); - } - pointsX[pointCount] = (float)element.vertex.point.x; - pointsY[pointCount] = (float)element.vertex.point.y; - pointCount++; - - if (i != 0) { - const auto &previousElement = pathElements[i - 1]; - if (previousElement.vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) { - } else { - if (pointsSize < pointCount + 1) { - pointsSize = (pointCount + 2) * 2; - pointsX = (float *)realloc(pointsX, pointsSize * 4); - pointsY = (float *)realloc(pointsY, pointsSize * 4); - } - pointsX[pointCount] = (float)previousElement.vertex.outTangent.x; - pointsY[pointCount] = (float)previousElement.vertex.outTangent.y; - pointCount++; - pointsX[pointCount] = (float)element.vertex.inTangent.x; - pointsY[pointCount] = (float)element.vertex.inTangent.y; - pointCount++; - } - } - } - } - - free(pointsX); - free(pointsY); - - if (pointCount == 0) { - return CGRect(0.0, 0.0, 0.0, 0.0); - } - - return calculateBoundingRectOpt(pointsX, pointsY, pointCount); -} - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CompoundBezierPath.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CompoundBezierPath.cpp deleted file mode 100644 index 31df7039562..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CompoundBezierPath.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "CompoundBezierPath.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CompoundBezierPath.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CompoundBezierPath.hpp deleted file mode 100644 index 58d02eb283f..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CompoundBezierPath.hpp +++ /dev/null @@ -1,163 +0,0 @@ -#ifndef CompoundBezierPath_hpp -#define CompoundBezierPath_hpp - -#include - -namespace lottie { - -/// A collection of BezierPath objects that can be trimmed and added. -/// -class CompoundBezierPath: public std::enable_shared_from_this { -public: - CompoundBezierPath() : - paths({}) { - } - - CompoundBezierPath(BezierPath const &path) : - paths({ path }) { - } - - CompoundBezierPath(std::vector paths_, std::optional length_) : - paths(paths_), _length(length_) { - } - - CompoundBezierPath(std::vector paths_) : - paths(paths_) { - } - -public: - std::vector paths; - - float length() { - if (_length.has_value()) { - return _length.value(); - } else { - float l = 0.0; - for (auto &path : paths) { - l += path.length(); - } - _length = l; - return l; - } - } - -private: - std::optional _length; - -public: - std::shared_ptr addingPath(BezierPath const &path) const { - auto newPaths = paths; - newPaths.push_back(path); - return std::make_shared(newPaths); - } - - void appendPath(BezierPath const &path) { - paths.push_back(path); - _length.reset(); - } - - std::shared_ptr combine(std::shared_ptr compoundBezier) { - auto newPaths = paths; - for (const auto &path : compoundBezier->paths) { - newPaths.push_back(path); - } - return std::make_shared(newPaths); - } - - std::shared_ptr trim(float fromPosition, float toPosition, float offset) { - if (fromPosition == toPosition) { - return std::make_shared(); - } - - float lengthValue = length(); - - /// Normalize lengths to the curve length. - float startPosition = fmod(fromPosition + offset, 1.0); - float endPosition = fmod(toPosition + offset, 1.0); - - if (startPosition < 0.0) { - startPosition = 1.0 + startPosition; - } - - if (endPosition < 0.0) { - endPosition = 1.0 + endPosition; - } - - if (startPosition == 1.0) { - startPosition = 0.0; - } - if (endPosition == 0.0) { - endPosition = 1.0; - } - - if ((startPosition == 0.0 && endPosition == 1.0) || - startPosition == endPosition || - (startPosition == 1.0 && endPosition == 0.0)) { - /// The trim encompasses the entire path. Return. - return shared_from_this(); - } - - std::vector positions; - if (endPosition < startPosition) { - positions = { - BezierTrimPathPosition(0.0, endPosition * lengthValue), - BezierTrimPathPosition(startPosition * lengthValue, lengthValue) - }; - } else { - positions = { BezierTrimPathPosition(startPosition * lengthValue, endPosition * lengthValue) }; - } - - auto compoundPath = std::make_shared(); - auto trim = positions[0]; - positions.erase(positions.begin()); - float pathStartPosition = 0.0; - - bool finishedTrimming = false; - int i = 0; - - while (!finishedTrimming) { - if (paths.size() <= i) { - /// Rounding errors - finishedTrimming = true; - continue; - } - auto path = paths[i]; - - auto pathEndPosition = pathStartPosition + path.length(); - - if (pathEndPosition < trim.start) { - /// Path is not included in the trim, continue. - pathStartPosition = pathEndPosition; - i = i + 1; - continue; - } else if (trim.start <= pathStartPosition && pathEndPosition <= trim.end) { - /// Full Path is inside of trim. Add full path. - compoundPath = compoundPath->addingPath(path); - } else { - auto trimPaths = path.trim(trim.start > pathStartPosition ? (trim.start - pathStartPosition) : 0, trim.end < pathEndPosition ? (trim.end - pathStartPosition) : path.length(), 0.0); - if (!trimPaths.empty()) { - compoundPath = compoundPath->addingPath(trimPaths[0]); - } - } - - if (trim.end <= pathEndPosition) { - /// We are done with the current trim. - /// Advance trim but remain on the same path in case the next trim overlaps it. - if (positions.size() > 0) { - trim = positions[0]; - positions.erase(positions.begin()); - } else { - finishedTrimming = true; - } - } else { - pathStartPosition = pathEndPosition; - i = i + 1; - } - } - return compoundPath; - } -}; - -} - -#endif /* CompoundBezierPath_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CoordinateSpace.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CoordinateSpace.cpp deleted file mode 100644 index 695b8dc4e91..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CoordinateSpace.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "CoordinateSpace.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CoordinateSpace.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CoordinateSpace.hpp deleted file mode 100644 index 37a7b7054ca..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CoordinateSpace.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef CoordinateSpace_hpp -#define CoordinateSpace_hpp - -namespace lottie { - -enum class CoordinateSpace { - Type2d, - Type3d -}; - -} - -#endif /* CoordinateSpace_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CurveVertex.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CurveVertex.cpp deleted file mode 100644 index cbdd11762a6..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/CurveVertex.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/PathElement.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/PathElement.cpp deleted file mode 100644 index af059629cd1..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/PathElement.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/DynamicProperties/AnimationKeypath.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/DynamicProperties/AnimationKeypath.cpp deleted file mode 100644 index 8f90bfc95e5..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/DynamicProperties/AnimationKeypath.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "AnimationKeypath.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/DynamicProperties/AnimationKeypath.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/DynamicProperties/AnimationKeypath.hpp deleted file mode 100644 index 8679106d129..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/DynamicProperties/AnimationKeypath.hpp +++ /dev/null @@ -1,51 +0,0 @@ -#ifndef AnimationKeypath_hpp -#define AnimationKeypath_hpp - -#include -#include - -namespace lottie { - -/// `AnimationKeypath` is an object that describes a keypath search for nodes in the -/// animation JSON. `AnimationKeypath` matches views and properties inside of `AnimationView` -/// to their backing `Animation` model by name. -/// -/// A keypath can be used to set properties on an existing animation, or can be validated -/// with an existing `Animation`. -/// -/// `AnimationKeypath` can describe a specific object, or can use wildcards for fuzzy matching -/// of objects. Acceptable wildcards are either "*" (star) or "**" (double star). -/// Single star will search a single depth for the next object. -/// Double star will search any depth. -/// -/// Read More at https://airbnb.io/lottie/#/ios?id=dynamic-animation-properties -/// -/// EG: -/// @"Layer.Shape Group.Stroke 1.Color" -/// Represents a specific color node on a specific stroke. -/// -/// @"**.Stroke 1.Color" -/// Represents the color node for every Stroke named "Stroke 1" in the animation. -class AnimationKeypath { -public: - /// Creates a keypath from a dot-separated string. The string is separated by "." - /*public init(keypath: String) { - keys = keypath.components(separatedBy: ".") - }*/ - - /// Creates a keypath from a list of strings. - AnimationKeypath(std::vector const &keys) : - _keys(keys) { - } - - std::vector const &keys() const { - return _keys; - } - -private: - std::vector _keys; -}; - -} - -#endif /* AnimationKeypath_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/DynamicProperties/AnyValueProvider.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/DynamicProperties/AnyValueProvider.cpp deleted file mode 100644 index f56124c957b..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/DynamicProperties/AnyValueProvider.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "AnyValueProvider.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/DynamicProperties/AnyValueProvider.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/DynamicProperties/AnyValueProvider.hpp deleted file mode 100644 index a9b708afbdc..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/DynamicProperties/AnyValueProvider.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef AnyValueProvider_hpp -#define AnyValueProvider_hpp - -#include "Lottie/Public/Primitives/AnimationTime.hpp" -#include "Lottie/Private/Model/Keyframes/KeyframeGroup.hpp" -#include "Lottie/Public/Primitives/AnyValue.hpp" - -#include -#include - -namespace lottie { - -/// `AnyValueProvider` is a protocol that return animation data for a property at a -/// given time. Every frame an `AnimationView` queries all of its properties and asks -/// if their ValueProvider has an update. If it does the AnimationView will read the -/// property and update that portion of the animation. -/// -/// Value Providers can be used to dynamically set animation properties at run time. -class AnyValueProvider { -public: - /// The Type of the value provider - virtual AnyValue::Type valueType() const = 0; - - /// Asks the provider if it has an update for the given frame. - virtual bool hasUpdate(AnimationFrameTime frame) const = 0; -}; - -/// A base protocol for strongly-typed Value Providers -template -class ValueProvider: public AnyValueProvider { -public: - /// Asks the provider to update the container with its value for the frame. - virtual T value(AnimationFrameTime frame) = 0; -}; - -} - -#endif /* AnyValueProvider_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/FontProvider/AnimationFontProvider.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/FontProvider/AnimationFontProvider.cpp deleted file mode 100644 index a6a4774df78..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/FontProvider/AnimationFontProvider.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "AnimationFontProvider.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/FontProvider/AnimationFontProvider.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/FontProvider/AnimationFontProvider.hpp deleted file mode 100644 index c14d46f8d9b..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/FontProvider/AnimationFontProvider.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef AnimationFontProvider_hpp -#define AnimationFontProvider_hpp - -#include "Lottie/Public/Primitives/CTFont.hpp" - -#include - -namespace lottie { - -/// Font provider is a protocol that is used to supply fonts to `AnimationView`. -/// -class AnimationFontProvider { -public: - virtual std::shared_ptr fontFor(std::string const &family, float size) = 0; -}; - -/// Default Font provider. -class DefaultFontProvider: public AnimationFontProvider { -public: - DefaultFontProvider() { - } - - virtual ~DefaultFontProvider() = default; - - virtual std::shared_ptr fontFor(std::string const &family, float size) override { - //CTFontCreateWithName(family as CFString, size, nil) - return nullptr; - } -}; - -} - -#endif /* AnimationFontProvider_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/ImageProvider/AnimationImageProvider.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/ImageProvider/AnimationImageProvider.hpp deleted file mode 100644 index 7efe74f8c8f..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/ImageProvider/AnimationImageProvider.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef AnimationImageProvider_hpp -#define AnimationImageProvider_hpp - -#include "Lottie/Public/Primitives/CALayer.hpp" -#include "Lottie/Private/Model/Assets/ImageAsset.hpp" - -namespace lottie { - -class AnimationImageProvider { -public: - virtual std::shared_ptr imageForAsset(ImageAsset const &imageAsset) = 0; -}; - -} - -#endif /* AnimationImageProvider_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Interpolatable.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Interpolatable.cpp deleted file mode 100644 index aa58f615c44..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Interpolatable.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "Interpolatable.hpp" - -namespace lottie { - -float remapFloat(float value, float fromLow, float fromHigh, float toLow, float toHigh) { - return toLow + (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow); -} - -float clampFloat(float value, float a, float b) { - float minValue = a <= b ? a : b; - float maxValue = a <= b ? b : a; - return std::max(std::min(value, maxValue), minValue); -} - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Interpolatable.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Interpolatable.hpp deleted file mode 100644 index b57223466ab..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Interpolatable.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef Interpolatable_hpp -#define Interpolatable_hpp - -#include - -namespace lottie { - -float remapFloat(float value, float fromLow, float fromHigh, float toLow, float toHigh); - -float clampFloat(float value, float a, float b); - -} - -#endif /* Interpolatable_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Keyframe.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Keyframe.cpp deleted file mode 100644 index e699e6daf81..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Keyframe.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "Keyframe.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Keyframe.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Keyframe.hpp deleted file mode 100644 index a3d12b226f7..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/Keyframe.hpp +++ /dev/null @@ -1,259 +0,0 @@ -#ifndef Keyframe_hpp -#define Keyframe_hpp - -#include "Lottie/Public/Primitives/AnimationTime.hpp" -#include -#include "Lottie/Public/Keyframes/Interpolatable.hpp" -#include "Lottie/Public/Keyframes/ValueInterpolators.hpp" - -#include - -namespace lottie { - -/// A keyframe with a single value, and timing information -/// about when the value should be displayed and how it -/// should be interpolated. -template -class Keyframe { -public: - /// Initialize a value-only keyframe with no time data. - Keyframe( - T const &value_, - std::optional spatialInTangent_, - std::optional spatialOutTangent_ - ) : - value(value_), - time(0), - isHold(true), - inTangent(std::nullopt), - outTangent(std::nullopt), - spatialInTangent(spatialInTangent_), - spatialOutTangent(spatialOutTangent_) { - } - - /// Initialize a keyframe - Keyframe( - T value_, - AnimationFrameTime time_, - bool isHold_, - std::optional inTangent_, - std::optional outTangent_, - std::optional spatialInTangent_, - std::optional spatialOutTangent_ - ) : - value(value_), - time(time_), - isHold(isHold_), - inTangent(inTangent_), - outTangent(outTangent_), - spatialInTangent(spatialInTangent_), - spatialOutTangent(spatialOutTangent_) { - } - - bool operator==(Keyframe const &rhs) { - return value == rhs.value - && time == rhs.time - && isHold == rhs.isHold - && inTangent == rhs.inTangent - && outTangent == rhs.outTangent - && spatialInTangent == rhs.spatialInTangent - && spatialOutTangent == rhs.spatialOutTangent; - } - - bool operator!=(Keyframe const &rhs) { - return !(*this == rhs); - } - -public: - T interpolate(Keyframe const &to, float progress) { - std::optional spatialOutTangent2d; - if (spatialOutTangent) { - spatialOutTangent2d = Vector2D(spatialOutTangent->x, spatialOutTangent->y); - } - std::optional spatialInTangent2d; - if (to.spatialInTangent) { - spatialInTangent2d = Vector2D(to.spatialInTangent->x, to.spatialInTangent->y); - } - return ValueInterpolator::interpolate(value, to.value, progress, spatialOutTangent2d, spatialInTangent2d); - } - - /// Interpolates the keyTime into a value from 0-1 - float interpolatedProgress(Keyframe const &to, float keyTime) { - float startTime = time; - float endTime = to.time; - if (keyTime <= startTime) { - return 0.0; - } - if (endTime <= keyTime) { - return 1.0; - } - - if (isHold) { - return 0.0; - } - - Vector2D outTanPoint = Vector2D::Zero(); - if (outTangent.has_value()) { - outTanPoint = outTangent.value(); - } - Vector2D inTanPoint = Vector2D(1.0, 1.0); - if (to.inTangent.has_value()) { - inTanPoint = to.inTangent.value(); - } - float progress = remapFloat(keyTime, startTime, endTime, 0.0f, 1.0f); - if (!outTanPoint.isZero() || inTanPoint != Vector2D(1.0f, 1.0f)) { - /// Cubic interpolation - progress = cubicBezierInterpolate(progress, Vector2D::Zero(), outTanPoint, inTanPoint, Vector2D(1.0, 1.0)); - } - return progress; - } - -public: - /// The value of the keyframe - T value; - /// The time in frames of the keyframe. - AnimationFrameTime time; - /// A hold keyframe freezes interpolation until the next keyframe that is not a hold. - bool isHold; - /// The in tangent for the time interpolation curve. - std::optional inTangent; - /// The out tangent for the time interpolation curve. - std::optional outTangent; - - /// The spatial in tangent of the vector. - std::optional spatialInTangent; - /// The spatial out tangent of the vector. - std::optional spatialOutTangent; -}; - -template -class KeyframeData { -public: - KeyframeData( - std::optional startValue_, - std::optional endValue_, - std::optional time_, - std::optional hold_, - std::optional inTangent_, - std::optional outTangent_, - std::optional spatialInTangent_, - std::optional spatialOutTangent_ - ) : - startValue(startValue_), - endValue(endValue_), - time(time_), - hold(hold_), - inTangent(inTangent_), - outTangent(outTangent_), - spatialInTangent(spatialInTangent_), - spatialOutTangent(spatialOutTangent_) { - } - - explicit KeyframeData(lottiejson11::Json const &json) noexcept(false) { - if (!json.is_object()) { - throw LottieParsingException(); - } - - if (const auto startValueData = getOptionalAny(json.object_items(), "s")) { - startValue = T(startValueData.value()); - } - - if (const auto endValueData = getOptionalAny(json.object_items(), "e")) { - endValue = T(endValueData.value()); - } - - if (const auto timeValue = getOptionalDouble(json.object_items(), "t")) { - time = (float)timeValue.value(); - } - - hold = getOptionalInt(json.object_items(), "h"); - - if (const auto inTangentData = getOptionalObject(json.object_items(), "i")) { - inTangent = Vector2D(inTangentData.value()); - } - - if (const auto outTangentData = getOptionalObject(json.object_items(), "o")) { - outTangent = Vector2D(outTangentData.value()); - } - - if (const auto spatialInTangentData = getOptionalAny(json.object_items(), "ti")) { - spatialInTangent = Vector3D(spatialInTangentData.value()); - } - - if (const auto spatialOutTangentData = getOptionalAny(json.object_items(), "to")) { - spatialOutTangent = Vector3D(spatialOutTangentData.value()); - } - - if (const auto nDataValue = getOptionalAny(json.object_items(), "n")) { - nData = nDataValue.value(); - } - } - - lottiejson11::Json::object toJson() const { - lottiejson11::Json::object result; - - if (startValue.has_value()) { - result.insert(std::make_pair("s", startValue->toJson())); - } - if (endValue.has_value()) { - result.insert(std::make_pair("e", endValue->toJson())); - } - if (time.has_value()) { - result.insert(std::make_pair("t", time.value())); - } - if (hold.has_value()) { - result.insert(std::make_pair("h", hold.value())); - } - if (inTangent.has_value()) { - result.insert(std::make_pair("i", inTangent->toJson())); - } - if (outTangent.has_value()) { - result.insert(std::make_pair("o", outTangent->toJson())); - } - if (spatialInTangent.has_value()) { - result.insert(std::make_pair("ti", spatialInTangent->toJson())); - } - if (spatialOutTangent.has_value()) { - result.insert(std::make_pair("to", spatialOutTangent->toJson())); - } - if (nData.has_value()) { - result.insert(std::make_pair("n", nData.value())); - } - - return result; - } - -public: - /// The start value of the keyframe - std::optional startValue; - /// The End value of the keyframe. Note: Newer versions animation json do not have this field. - std::optional endValue; - /// The time in frames of the keyframe. - std::optional time; - /// A hold keyframe freezes interpolation until the next keyframe that is not a hold. - std::optional hold; - - /// The in tangent for the time interpolation curve. - std::optional inTangent; - /// The out tangent for the time interpolation curve. - std::optional outTangent; - - /// The spacial in tangent of the vector. - std::optional spatialInTangent; - /// The spacial out tangent of the vector. - std::optional spatialOutTangent; - - std::optional nData; - - bool isHold() const { - if (hold.has_value()) { - return hold.value() > 0; - } else { - return false; - } - } -}; - -} - -#endif /* Keyframe_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.cpp deleted file mode 100644 index 650d25e0411..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "ValueInterpolators.hpp" - -#include - -namespace lottie { - -void batchInterpolate(std::vector const &from, std::vector const &to, BezierPath &resultPath, float amount) { - int elementCount = (int)from.size(); - if (elementCount > (int)to.size()) { - elementCount = (int)to.size(); - } - - static_assert(sizeof(PathElement) == 4 * 2 * 3); - - resultPath.setElementCount(elementCount); - float floatAmount = (float)amount; - vDSP_vintb((float *)&from[0], 1, (float *)&to[0], 1, &floatAmount, (float *)&resultPath.elements()[0], 1, elementCount * 2 * 3); -} - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.hpp deleted file mode 100644 index 4cc473bfc90..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.hpp +++ /dev/null @@ -1,227 +0,0 @@ -#ifndef ValueInterpolators_hpp -#define ValueInterpolators_hpp - -#include -#import -#include -#include "Lottie/Private/Model/Text/TextDocument.hpp" -#include "Lottie/Public/Primitives/GradientColorSet.hpp" -#include "Lottie/Public/Primitives/DashPattern.hpp" - -#include -#include - -namespace lottie { - -template -struct ValueInterpolator { -}; - -template<> -struct ValueInterpolator { -public: - static float interpolate(float value, float to, float amount, std::optional spatialOutTangent, std::optional spatialInTangent) { - return value + ((to - value) * amount); - } -}; - -template<> -struct ValueInterpolator { -public: - static Vector1D interpolate(Vector1D const &value, Vector1D const &to, float amount, std::optional spatialOutTangent, std::optional spatialInTangent) { - return Vector1D(ValueInterpolator::interpolate(value.value, to.value, amount, spatialOutTangent, spatialInTangent)); - } -}; - -template<> -struct ValueInterpolator { -public: - static Vector2D interpolate(Vector2D const &value, Vector2D const &to, float amount, Vector2D spatialOutTangent, Vector2D spatialInTangent) { - auto cp1 = value + spatialOutTangent; - auto cp2 = to + spatialInTangent; - - return value.interpolate(to, cp1, cp2, amount); - } - - static Vector2D interpolate(Vector2D const &value, Vector2D const &to, float amount) { - return value.interpolate(to, amount); - } -}; - -template<> -struct ValueInterpolator { -public: - static Vector3D interpolate(Vector3D const &value, Vector3D const &to, float amount, std::optional spatialOutTangent, std::optional spatialInTangent) { - if (spatialOutTangent && spatialInTangent) { - Vector2D from2d(value.x, value.y); - Vector2D to2d(to.x, to.y); - - auto cp1 = from2d + spatialOutTangent.value(); - auto cp2 = to2d + spatialInTangent.value(); - - Vector2D result2d = from2d.interpolate(to2d, cp1, cp2, amount); - - return Vector3D( - result2d.x, - result2d.y, - ValueInterpolator::interpolate(value.z, to.z, amount, spatialOutTangent, spatialInTangent) - ); - } - - return Vector3D( - ValueInterpolator::interpolate(value.x, to.x, amount, spatialOutTangent, spatialInTangent), - ValueInterpolator::interpolate(value.y, to.y, amount, spatialOutTangent, spatialInTangent), - ValueInterpolator::interpolate(value.z, to.z, amount, spatialOutTangent, spatialInTangent) - ); - } -}; - -template<> -struct ValueInterpolator { -public: - static Color interpolate(Color const &value, Color const &to, float amount, std::optional spatialOutTangent, std::optional spatialInTangent) { - return Color( - ValueInterpolator::interpolate(value.r, to.r, amount, spatialOutTangent, spatialInTangent), - ValueInterpolator::interpolate(value.g, to.g, amount, spatialOutTangent, spatialInTangent), - ValueInterpolator::interpolate(value.b, to.b, amount, spatialOutTangent, spatialInTangent), - ValueInterpolator::interpolate(value.a, to.a, amount, spatialOutTangent, spatialInTangent) - ); - } -}; - -void batchInterpolate(std::vector const &from, std::vector const &to, BezierPath &resultPath, float amount); - -template<> -struct ValueInterpolator { -public: - static CurveVertex interpolate(CurveVertex const &value, CurveVertex const &to, float amount, Vector2D spatialOutTangent, Vector2D spatialInTangent) { - return CurveVertex::absolute( - ValueInterpolator::interpolate(value.point, to.point, amount, spatialOutTangent, spatialInTangent), - ValueInterpolator::interpolate(value.inTangent, to.inTangent, amount, spatialOutTangent, spatialInTangent), - ValueInterpolator::interpolate(value.outTangent, to.outTangent, amount, spatialOutTangent, spatialInTangent) - ); - } - - static CurveVertex interpolate(CurveVertex const &value, CurveVertex const &to, float amount) { - return CurveVertex::absolute( - ValueInterpolator::interpolate(value.point, to.point, amount), - ValueInterpolator::interpolate(value.inTangent, to.inTangent, amount), - ValueInterpolator::interpolate(value.outTangent, to.outTangent, amount) - ); - } -}; - -template<> -struct ValueInterpolator { -public: - static BezierPath interpolate(BezierPath const &value, BezierPath const &to, float amount, std::optional spatialOutTangent, std::optional spatialInTangent) { - BezierPath newPath; - newPath.reserveCapacity(std::max(value.elements().size(), to.elements().size())); - //TODO:probably a bug in the upstream code, uncomment - //newPath.setClosed(value.closed()); - size_t elementCount = std::min(value.elements().size(), to.elements().size()); - - if (spatialInTangent && spatialOutTangent) { - Vector2D spatialInTangentValue = spatialInTangent.value(); - Vector2D spatialOutTangentValue = spatialOutTangent.value(); - - for (size_t i = 0; i < elementCount; i++) { - const auto &fromVertex = value.elements()[i].vertex; - const auto &toVertex = to.elements()[i].vertex; - - newPath.addVertex(ValueInterpolator::interpolate(fromVertex, toVertex, amount, spatialOutTangentValue, spatialInTangentValue)); - } - } else { - for (size_t i = 0; i < elementCount; i++) { - const auto &fromVertex = value.elements()[i].vertex; - const auto &toVertex = to.elements()[i].vertex; - - newPath.addVertex(ValueInterpolator::interpolate(fromVertex, toVertex, amount)); - } - } - return newPath; - } - - static void setInplace(BezierPath const &value, BezierPath &resultPath) { - resultPath.reserveCapacity(value.elements().size()); - resultPath.setElementCount(value.elements().size()); - resultPath.invalidateLength(); - - memcpy(resultPath.mutableElements().data(), value.elements().data(), value.elements().size() * sizeof(PathElement)); - } - - static void interpolateInplace(BezierPath const &value, BezierPath const &to, float amount, std::optional spatialOutTangent, std::optional spatialInTangent, BezierPath &resultPath) { - /*if (value.elements().size() != to.elements().size()) { - return to; - }*/ - - //TODO:probably a bug in the upstream code, uncomment - //newPath.setClosed(value.closed()); - - int elementCount = (int)std::min(value.elements().size(), to.elements().size()); - - resultPath.reserveCapacity(std::max(value.elements().size(), to.elements().size())); - resultPath.setElementCount(elementCount); - resultPath.invalidateLength(); - - if (spatialInTangent && spatialOutTangent) { - Vector2D spatialInTangentValue = spatialInTangent.value(); - Vector2D spatialOutTangentValue = spatialOutTangent.value(); - - for (int i = 0; i < elementCount; i++) { - const auto &fromVertex = value.elements()[i].vertex; - const auto &toVertex = to.elements()[i].vertex; - - auto vertex = ValueInterpolator::interpolate(fromVertex, toVertex, amount, spatialOutTangentValue, spatialInTangentValue); - - resultPath.updateVertex(vertex, i, false); - } - } else { - batchInterpolate(value.elements(), to.elements(), resultPath, amount); - } - } -}; - -template<> -struct ValueInterpolator { -public: - static TextDocument interpolate(TextDocument const &value, TextDocument const &to, float amount, std::optional spatialOutTangent, std::optional spatialInTangent) { - if (amount == 1.0) { - return to; - } else { - return value; - } - } -}; - -template<> -struct ValueInterpolator { -public: - static GradientColorSet interpolate(GradientColorSet const &value, GradientColorSet const &to, float amount, std::optional spatialOutTangent, std::optional spatialInTangent) { - assert(value.colors.size() == to.colors.size()); - std::vector colors; - size_t colorCount = std::min(value.colors.size(), to.colors.size()); - for (size_t i = 0; i < colorCount; i++) { - colors.push_back(ValueInterpolator::interpolate(value.colors[i], to.colors[i], amount, spatialOutTangent, spatialInTangent)); - } - return GradientColorSet(colors); - } -}; - -template<> -struct ValueInterpolator { -public: - static DashPattern interpolate(DashPattern const &value, DashPattern const &to, float amount, std::optional spatialOutTangent, std::optional spatialInTangent) { - assert(value.values.size() == to.values.size()); - std::vector values; - size_t colorCount = std::min(value.values.size(), to.values.size()); - for (size_t i = 0; i < colorCount; i++) { - values.push_back(ValueInterpolator::interpolate(value.values[i], to.values[i], amount, spatialOutTangent, spatialInTangent)); - } - return DashPattern(std::move(values)); - } -}; - -} - -#endif /* ValueInterpolators_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnimationTime.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnimationTime.cpp deleted file mode 100644 index c804b75b106..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnimationTime.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "AnimationTime.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnimationTime.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnimationTime.hpp deleted file mode 100644 index a2a7ec3fdd1..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnimationTime.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef AnimationTime_hpp -#define AnimationTime_hpp - -namespace lottie { - -/// Defines animation time in Frames (Seconds * Framerate). -typedef float AnimationFrameTime; - -/// Defines animation time by a progress from 0 (beginning of the animation) to 1 (end of the animation) -typedef float AnimationProgressTime; - -} - -#endif /* AnimationTime_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnyValue.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnyValue.cpp deleted file mode 100644 index 1af508b3de1..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnyValue.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "AnyValue.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnyValue.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnyValue.hpp deleted file mode 100644 index f730fad6528..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/AnyValue.hpp +++ /dev/null @@ -1,245 +0,0 @@ -#ifndef AnyValue_hpp -#define AnyValue_hpp - -#include -#import -#include -#include "Lottie/Private/Model/Text/TextDocument.hpp" -#include "Lottie/Private/Model/ShapeItems/GradientFill.hpp" -#include "Lottie/Private/Model/Objects/DashElement.hpp" - -#include -#include - -namespace lottie { - -class AnyValue { -public: - enum class Type { - Float, - Vector1D, - Vector2D, - Vector3D, - Color, - BezierPath, - TextDocument, - GradientColorSet, - DashPattern - }; - -public: - AnyValue(float value) : - _type(Type::Float), - _floatValue(value) { - } - - AnyValue(Vector1D const &value) : - _type(Type::Vector1D), - _vector1DValue(value) { - } - - AnyValue(Vector2D const &value) : - _type(Type::Vector2D), - _vector2DValue(value) { - } - - AnyValue(Vector3D const & value) : - _type(Type::Vector3D), - _vector3DValue(value) { - } - - AnyValue(Color const &value) : - _type(Type::Color), - _colorValue(value) { - } - - AnyValue(BezierPath const &value) : - _type(Type::BezierPath), - _bezierPathValue(value) { - } - - AnyValue(TextDocument const &value) : - _type(Type::TextDocument), - _textDocumentValue(value) { - } - - AnyValue(GradientColorSet const &value) : - _type(Type::GradientColorSet), - _gradientColorSetValue(value) { - } - - AnyValue(DashPattern const &value) : - _type(Type::DashPattern), - _dashPatternValue(value) { - } - - template::value>> - float get() { - return asFloat(); - } - - template::value>> - Vector1D get() { - return asVector1D(); - } - - template::value>> - Vector2D get() { - return asVector2D(); - } - - template::value>> - Vector3D get() { - return asVector3D(); - } - - template::value>> - Color get() { - return asColor(); - } - - template::value>> - BezierPath get() { - return asBezierPath(); - } - - template::value>> - TextDocument get() { - return asTextDocument(); - } - - template::value>> - GradientColorSet get() { - return asGradientColorSet(); - } - - template::value>> - DashPattern get() { - return asDashPattern(); - } - -public: - Type type() { - return _type; - } - - float asFloat() { - return _floatValue.value(); - } - - Vector1D asVector1D() { - return _vector1DValue.value(); - } - - Vector2D asVector2D() { - return _vector2DValue.value(); - } - - Vector3D asVector3D() { - return _vector3DValue.value(); - } - - Color asColor() { - return _colorValue.value(); - } - - BezierPath asBezierPath() { - return _bezierPathValue.value(); - } - - TextDocument asTextDocument() { - return _textDocumentValue.value(); - } - - GradientColorSet asGradientColorSet() { - return _gradientColorSetValue.value(); - } - - DashPattern asDashPattern() { - return _dashPatternValue.value(); - } - -private: - Type _type; - - std::optional _floatValue; - std::optional _vector1DValue; - std::optional _vector2DValue; - std::optional _vector3DValue; - std::optional _colorValue; - std::optional _bezierPathValue; - std::optional _textDocumentValue; - std::optional _gradientColorSetValue; - std::optional _dashPatternValue; -}; - -template -struct AnyValueType { -}; - -template<> -struct AnyValueType { - static AnyValue::Type type() { - return AnyValue::Type::Float; - } -}; - -template<> -struct AnyValueType { - static AnyValue::Type type() { - return AnyValue::Type::Vector1D; - } -}; - -template<> -struct AnyValueType { - static AnyValue::Type type() { - return AnyValue::Type::Vector2D; - } -}; - -template<> -struct AnyValueType { - static AnyValue::Type type() { - return AnyValue::Type::Vector3D; - } -}; - -template<> -struct AnyValueType { - static AnyValue::Type type() { - return AnyValue::Type::Color; - } -}; - -template<> -struct AnyValueType { - static AnyValue::Type type() { - return AnyValue::Type::BezierPath; - } -}; - -template<> -struct AnyValueType { - static AnyValue::Type type() { - return AnyValue::Type::TextDocument; - } -}; - -template<> -struct AnyValueType { - static AnyValue::Type type() { - return AnyValue::Type::GradientColorSet; - } -}; - -template<> -struct AnyValueType { - static AnyValue::Type type() { - return AnyValue::Type::DashPattern; - } -}; - -} - -#endif /* AnyValue_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayer.cpp deleted file mode 100644 index add81ca9734..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayer.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "CALayer.hpp" - -namespace lottie { - -std::shared_ptr CAShapeLayer::renderableItem() { - if (!_path) { - return nullptr; - } - - std::optional fill; - if (_fillColor) { - fill = ShapeRenderableItem::Fill( - _fillColor.value(), - _fillRule - ); - } - - std::optional stroke; - if (_strokeColor) { - stroke = ShapeRenderableItem::Stroke( - _strokeColor.value(), - _lineWidth, - _lineJoin, - _lineCap, - _lineDashPhase, - _dashPattern - ); - } - - return std::make_shared( - _path, - fill, - stroke - ); -} - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayer.hpp deleted file mode 100644 index c0c0a610062..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CALayer.hpp +++ /dev/null @@ -1,212 +0,0 @@ -#ifndef CALayer_hpp -#define CALayer_hpp - -#import -#include -#include -#include -#include "Lottie/Private/Model/ShapeItems/Fill.hpp" -#include "Lottie/Private/Model/Layers/LayerModel.hpp" -#include -#include "Lottie/Private/Model/ShapeItems/GradientFill.hpp" - -#include -#include -#include - -namespace lottie { - -class CALayer: public std::enable_shared_from_this { -public: - CALayer() { - } - - virtual ~CALayer() = default; - - void addSublayer(std::shared_ptr layer) { - _sublayers.push_back(layer); - } - - void insertSublayer(std::shared_ptr layer, int index) { - _sublayers.insert(_sublayers.begin() + index, layer); - } - - virtual bool implementsDraw() const { - return false; - } - - virtual bool isInvertedMatte() const { - return false; - } - - virtual std::shared_ptr renderableItem() { - return nullptr; - } - - bool isHidden() const { - return _isHidden; - } - void setIsHidden(bool isHidden) { - _isHidden = isHidden; - } - - float opacity() const { - return _opacity; - } - void setOpacity(float opacity) { - _opacity = opacity; - } - - Vector2D const &size() const { - return _size; - } - void setSize(Vector2D const &size) { - _size = size; - } - - Transform2D const &transform() const { - return _transform; - } - void setTransform(Transform2D const &transform) { - _transform = transform; - } - - std::shared_ptr const &mask() const { - return _mask; - } - void setMask(std::shared_ptr mask) { - _mask = mask; - } - - bool masksToBounds() const { - return _masksToBounds; - } - void setMasksToBounds(bool masksToBounds) { - _masksToBounds = masksToBounds; - } - - std::vector> const &sublayers() const { - return _sublayers; - } - - std::optional const &compositingFilter() const { - return _compositingFilter; - } - void setCompositingFilter(std::optional const &compositingFilter) { - _compositingFilter = compositingFilter; - } - -protected: - template - std::shared_ptr shared_from_base() { - return std::static_pointer_cast(shared_from_this()); - } - -private: - void removeSublayer(CALayer *layer) { - for (auto it = _sublayers.begin(); it != _sublayers.end(); it++) { - if (it->get() == layer) { - _sublayers.erase(it); - break; - } - } - } - -private: - std::vector> _sublayers; - bool _isHidden = false; - float _opacity = 1.0; - Vector2D _size = Vector2D(0.0, 0.0); - Transform2D _transform = Transform2D::identity(); - std::shared_ptr _mask; - bool _masksToBounds = false; - std::optional _compositingFilter; -}; - -class CAShapeLayer: public CALayer { -public: - CAShapeLayer() { - } - - virtual ~CAShapeLayer() = default; - - std::optional const &strokeColor() { - return _strokeColor; - } - void setStrokeColor(std::optional const &strokeColor) { - _strokeColor = strokeColor; - } - - std::optional const &fillColor() { - return _fillColor; - } - void setFillColor(std::optional const &fillColor) { - _fillColor = fillColor; - } - - FillRule fillRule() { - return _fillRule; - } - void setFillRule(FillRule fillRule) { - _fillRule = fillRule; - } - - std::shared_ptr const &path() const { - return _path; - } - void setPath(std::shared_ptr const &path) { - _path = path; - } - - float lineWidth() const { - return _lineWidth; - } - void setLineWidth(float lineWidth) { - _lineWidth = lineWidth; - } - - LineJoin lineJoin() const { - return _lineJoin; - } - void setLineJoin(LineJoin lineJoin) { - _lineJoin = lineJoin; - } - - LineCap lineCap() const { - return _lineCap; - } - void setLineCap(LineCap lineCap) { - _lineCap = lineCap; - } - - float lineDashPhase() const { - return _lineDashPhase; - } - void setLineDashPhase(float lineDashPhase) { - _lineDashPhase = lineDashPhase; - } - - std::vector const &dashPattern() const { - return _dashPattern; - } - void setDashPattern(std::vector const &dashPattern) { - _dashPattern = dashPattern; - } - - std::shared_ptr renderableItem() override; - -private: - std::optional _strokeColor; - std::optional _fillColor = Color(0.0, 0.0, 0.0, 1.0); - FillRule _fillRule = FillRule::NonZeroWinding; - std::shared_ptr _path; - float _lineWidth = 1.0; - LineJoin _lineJoin = LineJoin::Miter; - LineCap _lineCap = LineCap::Butt; - float _lineDashPhase = 0.0; - std::vector _dashPattern; -}; - -} - -#endif /* CALayer_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.cpp deleted file mode 100644 index e985e183926..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.cpp +++ /dev/null @@ -1,193 +0,0 @@ -#include - -#include - -namespace lottie { - -namespace { - -void addPointToBoundingRect(bool *isFirst, CGRect *rect, Vector2D const *point) { - if (*isFirst) { - *isFirst = false; - - rect->x = point->x; - rect->y = point->y; - rect->width = 0.0; - rect->height = 0.0; - - return; - } - if (point->x > rect->x + rect->width) { - rect->width = point->x - rect->x; - } - if (point->y > rect->y + rect->height) { - rect->height = point->y - rect->y; - } - if (point->x < rect->x) { - rect->width += rect->x - point->x; - rect->x = point->x; - } - if (point->y < rect->y) { - rect->height += rect->y - point->y; - rect->y = point->y; - } -} - -} - -Vector2D transformVector(Vector2D const &v, Transform2D const &m) { - float transformedX = m.rows().columns[0][0] * v.x + m.rows().columns[1][0] * v.y + m.rows().columns[2][0] * 1.0f; - float transformedY = m.rows().columns[0][1] * v.x + m.rows().columns[1][1] * v.y + m.rows().columns[2][1] * 1.0f; - return Vector2D(transformedX, transformedY); -} - -class CGPathImpl: public CGPath { -public: - CGPathImpl(); - virtual ~CGPathImpl(); - - virtual CGRect boundingBox() const override; - - virtual bool empty() const override; - - virtual std::shared_ptr copyUsingTransform(Transform2D const &transform) const override; - - virtual void addLineTo(Vector2D const &point) override; - virtual void addCurveTo(Vector2D const &point, Vector2D const &control1, Vector2D const &control2) override; - virtual void moveTo(Vector2D const &point) override; - virtual void closeSubpath() override; - virtual void addRect(CGRect const &rect) override; - virtual void addPath(std::shared_ptr const &path) override; - virtual bool isEqual(CGPath *other) const override; - virtual void enumerate(std::function) override; - -private: - std::vector _items; -}; - -CGPathImpl::CGPathImpl() { -} - -CGPathImpl::~CGPathImpl() { -} - -CGRect CGPathImpl::boundingBox() const { - bool isFirst = true; - CGRect result(0.0, 0.0, 0.0, 0.0); - - for (const auto &item : _items) { - switch (item.type) { - case CGPathItem::Type::MoveTo: { - addPointToBoundingRect(&isFirst, &result, &item.points[0]); - break; - } - case CGPathItem::Type::LineTo: { - addPointToBoundingRect(&isFirst, &result, &item.points[0]); - break; - } - case CGPathItem::Type::CurveTo: { - addPointToBoundingRect(&isFirst, &result, &item.points[0]); - addPointToBoundingRect(&isFirst, &result, &item.points[1]); - addPointToBoundingRect(&isFirst, &result, &item.points[2]); - break; - } - case CGPathItem::Type::Close: { - break; - } - default: { - break; - } - } - } - - return result; -} - -bool CGPathImpl::empty() const { - return _items.empty(); -} - -std::shared_ptr CGPathImpl::copyUsingTransform(Transform2D const &transform) const { - auto result = std::make_shared(); - - if (transform == Transform2D::identity()) { - result->_items = _items; - return result; - } - - result->_items.reserve(_items.capacity()); - for (auto &sourceItem : _items) { - CGPathItem &item = result->_items.emplace_back(sourceItem.type); - item.points[0] = transformVector(sourceItem.points[0], transform); - item.points[1] = transformVector(sourceItem.points[1], transform); - item.points[2] = transformVector(sourceItem.points[2], transform); - } - - return result; -} - -void CGPathImpl::addLineTo(Vector2D const &point) { - CGPathItem &item = _items.emplace_back(CGPathItem::Type::LineTo); - item.points[0] = point; -} - -void CGPathImpl::addCurveTo(Vector2D const &point, Vector2D const &control1, Vector2D const &control2) { - CGPathItem &item = _items.emplace_back(CGPathItem::Type::CurveTo); - item.points[0] = control1; - item.points[1] = control2; - item.points[2] = point; -} - -void CGPathImpl::moveTo(Vector2D const &point) { - CGPathItem &item = _items.emplace_back(CGPathItem::Type::MoveTo); - item.points[0] = point; -} - -void CGPathImpl::closeSubpath() { - _items.emplace_back(CGPathItem::Type::Close); -} - -void CGPathImpl::addRect(CGRect const &rect) { - assert(false); - //CGPathAddRect(_path, nil, ::CGRectMake(rect.x, rect.y, rect.width, rect.height)); -} - -void CGPathImpl::addPath(std::shared_ptr const &path) { - if (_items.size() == 0) { - _items = std::static_pointer_cast(path)->_items; - } else { - size_t totalItemCount = _items.size() + std::static_pointer_cast(path)->_items.size(); - if (_items.capacity() < totalItemCount) { - _items.reserve(totalItemCount); - } - for (const auto &item : std::static_pointer_cast(path)->_items) { - _items.push_back(item); - } - } -} - -bool CGPathImpl::isEqual(CGPath *other) const { - if (_items.size() != ((CGPathImpl *)other)->_items.size()) { - return false; - } - - for (size_t i = 0; i < _items.size(); i++) { - if (_items[i] != ((CGPathImpl *)other)->_items[i]) { - return false; - } - } - - return true; -} - -void CGPathImpl::enumerate(std::function f) { - for (const auto &item : _items) { - f(item); - } -} - -std::shared_ptr CGPath::makePath() { - return std::static_pointer_cast(std::make_shared()); -} - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.mm b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.mm deleted file mode 100644 index 93a5e242a10..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CGPath.mm +++ /dev/null @@ -1,223 +0,0 @@ -#include - -#include -#import - -namespace { - -void addPointToBoundingRect(bool *isFirst, CGRect *rect, CGPoint *point) { - if (*isFirst) { - *isFirst = false; - - rect->origin.x = point->x; - rect->origin.y = point->y; - rect->size.width = 0.0; - rect->size.height = 0.0; - - return; - } - if (point->x > rect->origin.x + rect->size.width) { - rect->size.width = point->x - rect->origin.x; - } - if (point->y > rect->origin.y + rect->size.height) { - rect->size.height = point->y - rect->origin.y; - } - if (point->x < rect->origin.x) { - rect->size.width += rect->origin.x - point->x; - rect->origin.x = point->x; - } - if (point->y < rect->origin.y) { - rect->size.height += rect->origin.y - point->y; - rect->origin.y = point->y; - } -} - -} - -CGRect calculatePathBoundingBox(CGPathRef path) { - __block CGRect result = CGRectMake(0.0, 0.0, 0.0, 0.0); - __block bool isFirst = true; - - CGPathApplyWithBlock(path, ^(const CGPathElement * _Nonnull element) { - switch (element->type) { - case kCGPathElementMoveToPoint: { - addPointToBoundingRect(&isFirst, &result, &element->points[0]); - break; - } - case kCGPathElementAddLineToPoint: { - addPointToBoundingRect(&isFirst, &result, &element->points[0]); - break; - } - case kCGPathElementAddCurveToPoint: { - addPointToBoundingRect(&isFirst, &result, &element->points[0]); - addPointToBoundingRect(&isFirst, &result, &element->points[1]); - addPointToBoundingRect(&isFirst, &result, &element->points[2]); - break; - } - case kCGPathElementAddQuadCurveToPoint: { - addPointToBoundingRect(&isFirst, &result, &element->points[0]); - addPointToBoundingRect(&isFirst, &result, &element->points[1]); - break; - } - case kCGPathElementCloseSubpath: { - break; - } - } - }); - - return result; -} - -namespace lottie { - -CGPathCocoaImpl::CGPathCocoaImpl() { - _path = CGPathCreateMutable(); -} - -CGPathCocoaImpl::CGPathCocoaImpl(CGMutablePathRef path) { - CFRetain(path); - _path = path; -} - -CGPathCocoaImpl::~CGPathCocoaImpl() { - CGPathRelease(_path); -} - -CGRect CGPathCocoaImpl::boundingBox() const { - auto rect = calculatePathBoundingBox(_path); - return CGRect(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height); -} - -bool CGPathCocoaImpl::empty() const { - return CGPathIsEmpty(_path); -} - -std::shared_ptr CGPathCocoaImpl::copyUsingTransform(Transform2D const &transform) const { - CGAffineTransform affineTransform = CGAffineTransformMake( - transform.rows().columns[0][0], transform.rows().columns[0][1], - transform.rows().columns[1][0], transform.rows().columns[1][1], - transform.rows().columns[2][0], transform.rows().columns[2][1] - ); - - CGPathRef resultPath = CGPathCreateCopyByTransformingPath(_path, &affineTransform); - if (resultPath == nil) { - return nullptr; - } - - CGMutablePathRef resultMutablePath = CGPathCreateMutableCopy(resultPath); - CGPathRelease(resultPath); - auto result = std::make_shared(resultMutablePath); - CGPathRelease(resultMutablePath); - - return result; -} - -void CGPathCocoaImpl::addLineTo(Vector2D const &point) { - CGPathAddLineToPoint(_path, nil, point.x, point.y); -} - -void CGPathCocoaImpl::addCurveTo(Vector2D const &point, Vector2D const &control1, Vector2D const &control2) { - CGPathAddCurveToPoint(_path, nil, control1.x, control1.y, control2.x, control2.y, point.x, point.y); -} - -void CGPathCocoaImpl::moveTo(Vector2D const &point) { - CGPathMoveToPoint(_path, nil, point.x, point.y); -} - -void CGPathCocoaImpl::closeSubpath() { - CGPathCloseSubpath(_path); -} - -void CGPathCocoaImpl::addRect(CGRect const &rect) { - CGPathAddRect(_path, nil, ::CGRectMake(rect.x, rect.y, rect.width, rect.height)); -} - -void CGPathCocoaImpl::addPath(std::shared_ptr const &path) { - if (CGPathIsEmpty(_path)) { - _path = CGPathCreateMutableCopy(std::static_pointer_cast(path)->_path); - } else { - CGPathAddPath(_path, nil, std::static_pointer_cast(path)->_path); - } -} - -CGPathRef CGPathCocoaImpl::nativePath() const { - return _path; -} - -bool CGPathCocoaImpl::isEqual(CGPath *other) const { - CGPathCocoaImpl *otherImpl = (CGPathCocoaImpl *)other; - return CGPathEqualToPath(_path, otherImpl->_path); -} - -void CGPathCocoaImpl::enumerate(std::function f) { - CGPathApplyWithBlock(_path, ^(const CGPathElement * _Nonnull element) { - CGPathItem item(CGPathItem::Type::MoveTo); - - switch (element->type) { - case kCGPathElementMoveToPoint: { - item.type = CGPathItem::Type::MoveTo; - item.points[0] = Vector2D(element->points[0].x, element->points[0].y); - f(item); - break; - } - case kCGPathElementAddLineToPoint: { - item.type = CGPathItem::Type::LineTo; - item.points[0] = Vector2D(element->points[0].x, element->points[0].y); - f(item); - break; - } - case kCGPathElementAddCurveToPoint: { - item.type = CGPathItem::Type::CurveTo; - item.points[0] = Vector2D(element->points[0].x, element->points[0].y); - item.points[1] = Vector2D(element->points[1].x, element->points[1].y); - item.points[2] = Vector2D(element->points[2].x, element->points[2].y); - f(item); - break; - } - case kCGPathElementAddQuadCurveToPoint: { - break; - } - case kCGPathElementCloseSubpath: { - item.type = CGPathItem::Type::Close; - f(item); - break; - } - } - }); -} - -void CGPathCocoaImpl::withNativePath(std::shared_ptr const &path, std::function f) { - CGMutablePathRef result = CGPathCreateMutable(); - - path->enumerate([result](CGPathItem const &element) { - switch (element.type) { - case CGPathItem::Type::MoveTo: { - CGPathMoveToPoint(result, nullptr, element.points[0].x, element.points[0].y); - break; - } - case CGPathItem::Type::LineTo: { - CGPathAddLineToPoint(result, nullptr, element.points[0].x, element.points[0].y); - break; - } - case CGPathItem::Type::CurveTo: { - CGPathAddCurveToPoint(result, nullptr, element.points[0].x, element.points[0].y, element.points[1].x, element.points[1].y, element.points[2].x, element.points[2].y); - break; - } - case CGPathItem::Type::Close: { - CGPathCloseSubpath(result); - break; - } - default: - break; - } - }); - - f(result); - CFRelease(result); -} - -/*std::shared_ptr CGPath::makePath() { - return std::static_pointer_cast(std::make_shared()); -}*/ - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CTFont.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CTFont.cpp deleted file mode 100644 index 24022559bfe..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CTFont.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "CTFont.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CTFont.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CTFont.hpp deleted file mode 100644 index 351d4bfc224..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/CTFont.hpp +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef CTFont_hpp -#define CTFont_hpp - -namespace lottie { - -class CTFont { - -}; - -} - -#endif /* CTFont_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Color.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Color.cpp deleted file mode 100644 index d627e71da43..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Color.cpp +++ /dev/null @@ -1,128 +0,0 @@ -#include - -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include - -namespace lottie { - -Color::Color(float r_, float g_, float b_, float a_, ColorFormatDenominator denominator) { - float denominatorValue = 1.0; - switch (denominator) { - case ColorFormatDenominator::One: { - denominatorValue = 1.0; - break; - } - case ColorFormatDenominator::OneHundred: { - denominatorValue = 100.0; - break; - } - case ColorFormatDenominator::TwoFiftyFive: { - denominatorValue = 255.0; - break; - } - } - - r = r_ / denominatorValue; - g = g_ / denominatorValue; - b = b_ / denominatorValue; - a = a_ / denominatorValue; -} - -Color::Color(lottiejson11::Json const &jsonAny) noexcept(false) : - r(0.0), g(0.0), b(0.0), a(0.0) { - if (!jsonAny.is_array()) { - throw LottieParsingException(); - } - - for (const auto &item : jsonAny.array_items()) { - if (!item.is_number()) { - throw LottieParsingException(); - } - } - - size_t index = 0; - - float r1 = 0.0; - if (index < jsonAny.array_items().size()) { - if (!jsonAny.array_items()[index].is_number()) { - throw LottieParsingException(); - } - r1 = jsonAny.array_items()[index].number_value(); - index++; - } - - float g1 = 0.0; - if (index < jsonAny.array_items().size()) { - if (!jsonAny.array_items()[index].is_number()) { - throw LottieParsingException(); - } - g1 = jsonAny.array_items()[index].number_value(); - index++; - } - - float b1 = 0.0; - if (index < jsonAny.array_items().size()) { - if (!jsonAny.array_items()[index].is_number()) { - throw LottieParsingException(); - } - b1 = jsonAny.array_items()[index].number_value(); - index++; - } - - float a1 = 0.0; - if (index < jsonAny.array_items().size()) { - if (!jsonAny.array_items()[index].is_number()) { - throw LottieParsingException(); - } - a1 = jsonAny.array_items()[index].number_value(); - index++; - } - - if (r1 > 1.0 && r1 > 1.0 && b1 > 1.0 && a1 > 1.0) { - r1 = r1 / 255.0; - g1 = g1 / 255.0; - b1 = b1 / 255.0; - a1 = a1 / 255.0; - } - - r = r1; - g = g1; - b = b1; - a = a1; -} - -lottiejson11::Json Color::toJson() const { - lottiejson11::Json::array result; - - result.push_back(lottiejson11::Json(r)); - result.push_back(lottiejson11::Json(g)); - result.push_back(lottiejson11::Json(b)); - result.push_back(lottiejson11::Json(a)); - - return result; -} - -Color Color::fromString(std::string const &string) { - if (string.empty()) { - return Color(0.0, 0.0, 0.0, 0.0); - } - - std::string workString = string; - if (workString[0] == '#') { - workString.erase(workString.begin()); - } - - std::istringstream converter(workString); - uint32_t rgbValue; - converter >> std::hex >> rgbValue; - - return Color( - ((float)((rgbValue & 0xFF0000) >> 16)) / 255.0, - ((float)((rgbValue & 0x00FF00) >> 8)) / 255.0, - ((float)(rgbValue & 0x0000FF)) / 255.0, - 1.0 - ); -} - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/DashPattern.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/DashPattern.cpp deleted file mode 100644 index 33426de3384..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/DashPattern.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "DashPattern.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/DashPattern.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/DashPattern.hpp deleted file mode 100644 index e6255ec8212..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/DashPattern.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef DashPattern_hpp -#define DashPattern_hpp - -#include - -namespace lottie { - -struct DashPattern { - DashPattern(std::vector &&values_) : - values(std::move(values_)) { - } - - std::vector values; -}; - -} - -#endif /* DashPattern_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/GradientColorSet.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/GradientColorSet.cpp deleted file mode 100644 index 3a92ce0d41b..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/GradientColorSet.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "GradientColorSet.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/GradientColorSet.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/GradientColorSet.hpp deleted file mode 100644 index abf576690f8..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/GradientColorSet.hpp +++ /dev/null @@ -1,42 +0,0 @@ -#ifndef GradientColorSet_hpp -#define GradientColorSet_hpp - -#include "Lottie/Private/Parsing/JsonParsing.hpp" - -#include - -namespace lottie { - -struct GradientColorSet { - GradientColorSet() { - } - - explicit GradientColorSet(lottiejson11::Json const &jsonAny) noexcept(false) { - if (!jsonAny.is_array()) { - throw LottieParsingException(); - } - - for (const auto &item : jsonAny.array_items()) { - if (!item.is_number()) { - throw LottieParsingException(); - } - colors.push_back(item.number_value()); - } - } - - lottiejson11::Json toJson() const { - lottiejson11::Json::array result; - - for (auto value : colors) { - result.push_back(value); - } - - return result; - } - - std::vector colors; -}; - -} - -#endif /* GradientColorSet_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Vectors.mm b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Vectors.mm deleted file mode 100644 index 3695d35aa0d..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Primitives/Vectors.mm +++ /dev/null @@ -1,912 +0,0 @@ -#include -#include - -#include "Lottie/Private/Parsing/JsonParsing.hpp" -#include "Lottie/Public/Keyframes/Interpolatable.hpp" - -#include - -#import - -namespace lottie { - -/*explicit Transform2D(Transform3D const &t) { - CGAffineTransform at = CATransform3DGetAffineTransform(nativeTransform(t)); - _rows.columns[0] = simd_make_float3(at.a, at.b, 0.0); - _rows.columns[1] = simd_make_float3(at.c, at.d, 0.0); - _rows.columns[2] = simd_make_float3(at.tx, at.ty, 1.0); -} - - Transform3D transform3D() { - CGAffineTransform at = CGAffineTransformMake( - _rows.columns[0][0], _rows.columns[0][1], - _rows.columns[1][0], _rows.columns[1][1], - _rows.columns[2][0], _rows.columns[2][1] - ); - return fromNativeTransform(CATransform3DMakeAffineTransform(at)); - }*/ - -/*struct Transform3D { - float m11, m12, m13, m14; - float m21, m22, m23, m24; - float m31, m32, m33, m34; - float m41, m42, m43, m44; - - Transform3D( - float m11_, float m12_, float m13_, float m14_, - float m21_, float m22_, float m23_, float m24_, - float m31_, float m32_, float m33_, float m34_, - float m41_, float m42_, float m43_, float m44_ - ) : - m11(m11_), m12(m12_), m13(m13_), m14(m14_), - m21(m21_), m22(m22_), m23(m23_), m24(m24_), - m31(m31_), m32(m32_), m33(m33_), m34(m34_), - m41(m41_), m42(m42_), m43(m43_), m44(m44_) { - } - - bool operator==(Transform3D const &rhs) const { - return m11 == rhs.m11 && m12 == rhs.m12 && m13 == rhs.m13 && m14 == rhs.m14 && - m21 == rhs.m21 && m22 == rhs.m22 && m23 == rhs.m23 && m24 == rhs.m24 && - m31 == rhs.m31 && m32 == rhs.m32 && m33 == rhs.m33 && m34 == rhs.m34 && - m41 == rhs.m41 && m42 == rhs.m42 && m43 == rhs.m43 && m44 == rhs.m44; - } - - bool operator!=(Transform3D const &rhs) const { - return !(*this == rhs); - } - - inline bool isIdentity() const { - return m11 == 1.0 && m12 == 0.0 && m13 == 0.0 && m14 == 0.0 && - m21 == 0.0 && m22 == 1.0 && m23 == 0.0 && m24 == 0.0 && - m31 == 0.0 && m32 == 0.0 && m33 == 1.0 && m34 == 0.0 && - m41 == 0.0 && m42 == 0.0 && m43 == 0.0 && m44 == 1.0; - } - - static Transform3D makeTranslation(float tx, float ty, float tz) { - return Transform3D( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - tx, ty, tz, 1 - ); - } - - static Transform3D makeScale(float sx, float sy, float sz) { - return Transform3D( - sx, 0, 0, 0, - 0, sy, 0, 0, - 0, 0, sz, 0, - 0, 0, 0, 1 - ); - } - - static Transform3D makeRotation(float radians); - - static Transform3D makeSkew(float skew, float skewAxis) { - float mCos = cos(degreesToRadians(skewAxis)); - float mSin = sin(degreesToRadians(skewAxis)); - float aTan = tan(degreesToRadians(skew)); - - Transform3D transform1( - mCos, - mSin, - 0.0, - 0.0, - -mSin, - mCos, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0 - ); - - Transform3D transform2( - 1.0, - 0.0, - 0.0, - 0.0, - aTan, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0 - ); - - Transform3D transform3( - mCos, - -mSin, - 0.0, - 0.0, - mSin, - mCos, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0 - ); - - return transform3 * transform2 * transform1; - } - - static Transform3D makeTransform( - Vector2D const &anchor, - Vector2D const &position, - Vector2D const &scale, - float rotation, - std::optional skew, - std::optional skewAxis - ) { - Transform3D result = Transform3D::identity(); - if (skew.has_value() && skewAxis.has_value()) { - result = Transform3D::identity().translated(position).rotated(rotation).skewed(-skew.value(), skewAxis.value()).scaled(Vector2D(scale.x * 0.01, scale.y * 0.01)).translated(Vector2D(-anchor.x, -anchor.y)); - } else { - result = Transform3D::identity().translated(position).rotated(rotation).scaled(Vector2D(scale.x * 0.01, scale.y * 0.01)).translated(Vector2D(-anchor.x, -anchor.y)); - } - - return result; - } - - Transform3D rotated(float degrees) const; - - Transform3D translated(Vector2D const &translation) const; - - Transform3D scaled(Vector2D const &scale) const; - - Transform3D skewed(float skew, float skewAxis) const { - return Transform3D::makeSkew(skew, skewAxis) * (*this); - } - - static Transform3D identity() { - return Transform3D( - 1.0f, 0.0f, 0.0f, 0.0f, - 0.0f, 1.0f, 0.0f, 0.0f, - 0.0f, 0.0f, 1.0f, 0.0f, - 0.0f, 0.0f, 0.0f, 1.0f - ); - } - - Transform3D operator*(Transform3D const &b) const; -};*/ - -/*Transform2D t2d(Transform3D const &testMatrix) { - ::CATransform3D nativeTest; - - nativeTest.m11 = testMatrix.m11; - nativeTest.m12 = testMatrix.m12; - nativeTest.m13 = testMatrix.m13; - nativeTest.m14 = testMatrix.m14; - - nativeTest.m21 = testMatrix.m21; - nativeTest.m22 = testMatrix.m22; - nativeTest.m23 = testMatrix.m23; - nativeTest.m24 = testMatrix.m24; - - nativeTest.m31 = testMatrix.m31; - nativeTest.m32 = testMatrix.m32; - nativeTest.m33 = testMatrix.m33; - nativeTest.m34 = testMatrix.m34; - - nativeTest.m41 = testMatrix.m41; - nativeTest.m42 = testMatrix.m42; - nativeTest.m43 = testMatrix.m43; - nativeTest.m44 = testMatrix.m44; - - CGAffineTransform at = CATransform3DGetAffineTransform(nativeTest); - Transform2D result = Transform2D::identity(); - simd_float3x3 *rows = (simd_float3x3 *)&result.rows(); - rows->columns[0] = simd_make_float3(at.a, at.b, 0.0); - rows->columns[1] = simd_make_float3(at.c, at.d, 0.0); - rows->columns[2] = simd_make_float3(at.tx, at.ty, 1.0); - - return result; -} - -Transform3D t3d(Transform2D const &t) { - CGAffineTransform at = CGAffineTransformMake( - t.rows().columns[0][0], t.rows().columns[0][1], - t.rows().columns[1][0], t.rows().columns[1][1], - t.rows().columns[2][0], t.rows().columns[2][1] - ); - ::CATransform3D value = CATransform3DMakeAffineTransform(at); - - Transform3D result = Transform3D::identity(); - result.m11 = value.m11; - result.m12 = value.m12; - result.m13 = value.m13; - result.m14 = value.m14; - - result.m21 = value.m21; - result.m22 = value.m22; - result.m23 = value.m23; - result.m24 = value.m24; - - result.m31 = value.m31; - result.m32 = value.m32; - result.m33 = value.m33; - result.m34 = value.m34; - - result.m41 = value.m41; - result.m42 = value.m42; - result.m43 = value.m43; - result.m44 = value.m44; - - return result; -} - -Transform3D Transform3D::operator*(Transform3D const &b) const { - if (isIdentity()) { - return b; - } - if (b.isIdentity()) { - return *this; - } - - return t3d((t2d(*this) * t2d(b))); -}*/ - -Vector1D::Vector1D(lottiejson11::Json const &json) noexcept(false) { - if (json.is_number()) { - value = json.number_value(); - } else if (json.is_array()) { - if (json.array_items().empty()) { - throw LottieParsingException(); - } - if (!json.array_items()[0].is_number()) { - throw LottieParsingException(); - } - value = json.array_items()[0].number_value(); - } else { - throw LottieParsingException(); - } -} - -lottiejson11::Json Vector1D::toJson() const { - return lottiejson11::Json(value); -} - -Vector2D::Vector2D(lottiejson11::Json const &json) noexcept(false) { - x = 0.0; - y = 0.0; - - if (json.is_array()) { - int index = 0; - - if (json.array_items().size() > index) { - if (!json.array_items()[index].is_number()) { - throw LottieParsingException(); - } - x = json.array_items()[index].number_value(); - index++; - } - - if (json.array_items().size() > index) { - if (!json.array_items()[index].is_number()) { - throw LottieParsingException(); - } - y = json.array_items()[index].number_value(); - index++; - } - } else if (json.is_object()) { - auto xAny = getAny(json.object_items(), "x"); - if (xAny.is_number()) { - x = xAny.number_value(); - } else if (xAny.is_array()) { - if (xAny.array_items().empty()) { - throw LottieParsingException(); - } - if (!xAny.array_items()[0].is_number()) { - throw LottieParsingException(); - } - x = xAny.array_items()[0].number_value(); - } - - auto yAny = getAny(json.object_items(), "y"); - if (yAny.is_number()) { - y = yAny.number_value(); - } else if (yAny.is_array()) { - if (yAny.array_items().empty()) { - throw LottieParsingException(); - } - if (!yAny.array_items()[0].is_number()) { - throw LottieParsingException(); - } - y = yAny.array_items()[0].number_value(); - } - } else { - throw LottieParsingException(); - } -} - -lottiejson11::Json Vector2D::toJson() const { - lottiejson11::Json::object result; - - result.insert(std::make_pair("x", x)); - result.insert(std::make_pair("y", y)); - - return lottiejson11::Json(result); -} - -Vector3D::Vector3D(lottiejson11::Json const &json) noexcept(false) { - if (!json.is_array()) { - throw LottieParsingException(); - } - - int index = 0; - - x = 0.0; - y = 0.0; - z = 0.0; - - if (json.array_items().size() > index) { - if (!json.array_items()[index].is_number()) { - throw LottieParsingException(); - } - x = json.array_items()[index].number_value(); - index++; - } - - if (json.array_items().size() > index) { - if (!json.array_items()[index].is_number()) { - throw LottieParsingException(); - } - y = json.array_items()[index].number_value(); - index++; - } - - if (json.array_items().size() > index) { - if (!json.array_items()[index].is_number()) { - throw LottieParsingException(); - } - z = json.array_items()[index].number_value(); - index++; - } -} - -lottiejson11::Json Vector3D::toJson() const { - lottiejson11::Json::array result; - - result.push_back(lottiejson11::Json(x)); - result.push_back(lottiejson11::Json(y)); - result.push_back(lottiejson11::Json(z)); - - return lottiejson11::Json(result); -} - -Transform2D Transform2D::_identity = Transform2D( - simd_float3x3({ - simd_make_float3(1.0f, 0.0f, 0.0f), - simd_make_float3(0.0f, 1.0f, 0.0f), - simd_make_float3(0.0f, 0.0f, 1.0f) - }) -); - -Transform2D Transform2D::makeTranslation(float tx, float ty) { - return Transform2D(simd_float3x3({ - simd_make_float3(1.0f, 0.0f, 0.0f), - simd_make_float3(0.0f, 1.0f, 0.0f), - simd_make_float3(tx, ty, 1.0f) - })); -} - -Transform2D Transform2D::makeScale(float sx, float sy) { - return Transform2D(simd_float3x3({ - simd_make_float3(sx, 0.0f, 0.0f), - simd_make_float3(0.0f, sy, 0.0f), - simd_make_float3(0.0f, 0.0f, 1.0f) - })); -} - -Transform2D Transform2D::makeRotation(float radians) { - float c = cos(radians); - float s = sin(radians); - - return Transform2D(simd_float3x3({ - simd_make_float3(c, s, 0.0f), - simd_make_float3(-s, c, 0.0f), - simd_make_float3(0.0f, 0.0f, 1.0f) - })); -} - -Transform2D Transform2D::makeSkew(float skew, float skewAxis) { - if (std::abs(skew) <= FLT_EPSILON && std::abs(skewAxis) <= FLT_EPSILON) { - return Transform2D::identity(); - } - - float mCos = cos(degreesToRadians(skewAxis)); - float mSin = sin(degreesToRadians(skewAxis)); - float aTan = tan(degreesToRadians(skew)); - - simd_float3x3 simd1 = simd_float3x3({ - simd_make_float3(mCos, -mSin, 0.0), - simd_make_float3(mSin, mCos, 0.0), - simd_make_float3(0.0, 0.0, 1.0) - }); - - simd_float3x3 simd2 = simd_float3x3({ - simd_make_float3(1.0, 0.0, 0.0), - simd_make_float3(aTan, 1.0, 0.0), - simd_make_float3(0.0, 0.0, 1.0) - }); - - simd_float3x3 simd3 = simd_float3x3({ - simd_make_float3(mCos, mSin, 0.0), - simd_make_float3(-mSin, mCos, 0.0), - simd_make_float3(0.0, 0.0, 1.0) - }); - - simd_float3x3 result = simd_mul(simd_mul(simd3, simd2), simd1); - Transform2D resultTransform(result); - - return resultTransform; -} - -Transform2D Transform2D::makeTransform( - Vector2D const &anchor, - Vector2D const &position, - Vector2D const &scale, - float rotation, - std::optional skew, - std::optional skewAxis -) { - Transform2D result = Transform2D::identity(); - if (skew.has_value() && skewAxis.has_value()) { - result = Transform2D::identity().translated(position).rotated(rotation).skewed(-skew.value(), skewAxis.value()).scaled(Vector2D(scale.x * 0.01, scale.y * 0.01)).translated(Vector2D(-anchor.x, -anchor.y)); - } else { - result = Transform2D::identity().translated(position).rotated(rotation).scaled(Vector2D(scale.x * 0.01, scale.y * 0.01)).translated(Vector2D(-anchor.x, -anchor.y)); - } - - return result; -} - -Transform2D Transform2D::rotated(float degrees) const { - return Transform2D::makeRotation(degreesToRadians(degrees)) * (*this); -} - -Transform2D Transform2D::translated(Vector2D const &translation) const { - return Transform2D::makeTranslation(translation.x, translation.y) * (*this); -} - -Transform2D Transform2D::scaled(Vector2D const &scale) const { - return Transform2D::makeScale(scale.x, scale.y) * (*this); -} - -Transform2D Transform2D::skewed(float skew, float skewAxis) const { - return Transform2D::makeSkew(skew, skewAxis) * (*this); -} - -float interpolate(float value, float to, float amount) { - return value + ((to - value) * amount); -} - -Vector1D interpolate( - Vector1D const &from, - Vector1D const &to, - float amount -) { - return Vector1D(interpolate(from.value, to.value, amount)); -} - -Vector2D interpolate( - Vector2D const &from, - Vector2D const &to, - float amount -) { - return Vector2D(interpolate(from.x, to.x, amount), interpolate(from.y, to.y, amount)); -} - - -Vector3D interpolate( - Vector3D const &from, - Vector3D const &to, - float amount -) { - return Vector3D(interpolate(from.x, to.x, amount), interpolate(from.y, to.y, amount), interpolate(from.z, to.z, amount)); -} - -static float cubicRoot(float value) { - return pow(value, 1.0 / 3.0); -} - -static float SolveQuadratic(float a, float b, float c) { - float result = (-b + sqrt((b * b) - 4 * a * c)) / (2 * a); - if (isInRangeOrEqual(result, 0.0, 1.0)) { - return result; - } - - result = (-b - sqrt((b * b) - 4 * a * c)) / (2 * a); - if (isInRangeOrEqual(result, 0.0, 1.0)) { - return result; - } - - return -1.0; -} - -inline bool isApproximatelyEqual(float value, float other) { - return std::abs(value - other) <= FLT_EPSILON; -} - -static float SolveCubic(float a, float b, float c, float d) { - if (isApproximatelyEqual(a, 0.0f)) { - return SolveQuadratic(b, c, d); - } - if (isApproximatelyEqual(d, 0.0f)) { - return 0.0; - } - b /= a; - c /= a; - d /= a; - float q = (3.0 * c - (b * b)) / 9.0; - float r = (-27.0 * d + b * (9.0 * c - 2.0 * (b * b))) / 54.0; - float disc = (q * q * q) + (r * r); - float term1 = b / 3.0; - - if (disc > 0.0) { - float s = r + sqrt(disc); - s = (s < 0) ? -cubicRoot(-s) : cubicRoot(s); - float t = r - sqrt(disc); - t = (t < 0) ? -cubicRoot(-t) : cubicRoot(t); - - float result = -term1 + s + t; - if (isInRangeOrEqual(result, 0.0, 1.0)) { - return result; - } - } else if (isApproximatelyEqual(disc, 0.0f)) { - float r13 = (r < 0) ? -cubicRoot(-r) : cubicRoot(r); - - float result = -term1 + 2.0 * r13; - if (isInRangeOrEqual(result, 0.0, 1.0)) { - return result; - } - - result = -(r13 + term1); - if (isInRangeOrEqual(result, 0.0, 1.0)) { - return result; - } - } else { - q = -q; - float dum1 = q * q * q; - dum1 = acos(r / sqrt(dum1)); - float r13 = 2.0 * sqrt(q); - - float result = -term1 + r13 * cos(dum1 / 3.0); - if (isInRangeOrEqual(result, 0.0, 1.0)) { - return result; - } - result = -term1 + r13 * cos((dum1 + 2.0 * M_PI) / 3.0); - if (isInRangeOrEqual(result, 0.0, 1.0)) { - return result; - } - result = -term1 + r13 * cos((dum1 + 4.0 * M_PI) / 3.0); - if (isInRangeOrEqual(result, 0.0, 1.0)) { - return result; - } - } - - return -1.0; -} - -float cubicBezierInterpolate(float value, Vector2D const &P0, Vector2D const &P1, Vector2D const &P2, Vector2D const &P3) { - float t = 0.0; - if (isApproximatelyEqual(value, P0.x)) { - // Handle corner cases explicitly to prevent rounding errors - t = 0.0; - } else if (isApproximatelyEqual(value, P3.x)) { - t = 1.0; - } else { - // Calculate t - float a = -P0.x + 3 * P1.x - 3 * P2.x + P3.x; - float b = 3 * P0.x - 6 * P1.x + 3 * P2.x; - float c = -3 * P0.x + 3 * P1.x; - float d = P0.x - value; - float tTemp = SolveCubic(a, b, c, d); - if (isApproximatelyEqual(tTemp, -1.0f)) { - return -1.0; - } - t = tTemp; - } - - // Calculate y from t - float oneMinusT = 1.0 - t; - return (oneMinusT * oneMinusT * oneMinusT) * P0.y + 3 * t * (oneMinusT * oneMinusT) * P1.y + 3 * (t * t) * (1 - t) * P2.y + (t * t * t) * P3.y; -} - -struct InterpolationPoint2D { - InterpolationPoint2D(Vector2D const point_, float distance_) : - point(point_), distance(distance_) { - } - - Vector2D point; - float distance; -}; - -namespace { - float interpolateFloat(float value, float to, float amount) { - return value + ((to - value) * amount); - } -} - -Vector2D Vector2D::pointOnPath(Vector2D const &to, Vector2D const &outTangent, Vector2D const &inTangent, float amount) const { - auto a = interpolate(outTangent, amount); - auto b = outTangent.interpolate(inTangent, amount); - auto c = inTangent.interpolate(to, amount); - auto d = a.interpolate(b, amount); - auto e = b.interpolate(c, amount); - auto f = d.interpolate(e, amount); - return f; -} - -Vector2D Vector2D::interpolate(Vector2D const &to, float amount) const { - return Vector2D( - interpolateFloat(x, to.x, amount), - interpolateFloat(y, to.y, amount) - ); -} - -Vector2D Vector2D::interpolate( - Vector2D const &to, - Vector2D const &outTangent, - Vector2D const &inTangent, - float amount, - int maxIterations, - int samples, - float accuracy -) const { - if (amount == 0.0) { - return *this; - } - if (amount == 1.0) { - return to; - } - - if (colinear(outTangent, inTangent) && outTangent.colinear(inTangent, to)) { - return interpolate(to, amount); - } - - float step = 1.0 / (float)samples; - - std::vector points; - points.push_back(InterpolationPoint2D(*this, 0.0)); - float totalLength = 0.0; - - Vector2D previousPoint = *this; - float previousAmount = 0.0; - - int closestPoint = 0; - - while (previousAmount < 1.0) { - previousAmount = previousAmount + step; - - if (previousAmount < amount) { - closestPoint = closestPoint + 1; - } - - auto newPoint = pointOnPath(to, outTangent, inTangent, previousAmount); - auto distance = previousPoint.distanceTo(newPoint); - totalLength = totalLength + distance; - points.push_back(InterpolationPoint2D(newPoint, totalLength)); - previousPoint = newPoint; - } - - float accurateDistance = amount * totalLength; - auto point = points[closestPoint]; - - bool foundPoint = false; - - float pointAmount = ((float)closestPoint) * step; - float nextPointAmount = pointAmount + step; - - int refineIterations = 0; - while (!foundPoint) { - refineIterations = refineIterations + 1; - /// First see if the next point is still less than the projected length. - auto nextPoint = points[std::min(closestPoint + 1, (int)points.size() - 1)]; - if (nextPoint.distance < accurateDistance) { - point = nextPoint; - closestPoint = closestPoint + 1; - pointAmount = ((float)closestPoint) * step; - nextPointAmount = pointAmount + step; - if (closestPoint == (int)points.size()) { - foundPoint = true; - } - continue; - } - if (accurateDistance < point.distance) { - closestPoint = closestPoint - 1; - if (closestPoint < 0) { - foundPoint = true; - continue; - } - point = points[closestPoint]; - pointAmount = ((float)closestPoint) * step; - nextPointAmount = pointAmount + step; - continue; - } - - /// Now we are certain the point is the closest point under the distance - auto pointDiff = nextPoint.distance - point.distance; - auto proposedPointAmount = remapFloat((accurateDistance - point.distance) / pointDiff, 0.0, 1.0, pointAmount, nextPointAmount); - - auto newPoint = pointOnPath(to, outTangent, inTangent, proposedPointAmount); - auto newDistance = point.distance + point.point.distanceTo(newPoint); - pointAmount = proposedPointAmount; - point = InterpolationPoint2D(newPoint, newDistance); - if (accurateDistance - newDistance <= accuracy || - newDistance - accurateDistance <= accuracy) { - foundPoint = true; - } - - if (refineIterations == maxIterations) { - foundPoint = true; - } - } - return point.point; -} - -::CATransform3D nativeTransform(Transform2D const &value) { - CGAffineTransform at = CGAffineTransformMake( - value.rows().columns[0][0], value.rows().columns[0][1], - value.rows().columns[1][0], value.rows().columns[1][1], - value.rows().columns[2][0], value.rows().columns[2][1] - ); - return CATransform3DMakeAffineTransform(at); - - /*::CATransform3D result; - - result.m11 = value.m11; - result.m12 = value.m12; - result.m13 = value.m13; - result.m14 = value.m14; - - result.m21 = value.m21; - result.m22 = value.m22; - result.m23 = value.m23; - result.m24 = value.m24; - - result.m31 = value.m31; - result.m32 = value.m32; - result.m33 = value.m33; - result.m34 = value.m34; - - result.m41 = value.m41; - result.m42 = value.m42; - result.m43 = value.m43; - result.m44 = value.m44; - - return result;*/ -} - -Transform2D fromNativeTransform(::CATransform3D const &value) { - CGAffineTransform at = CATransform3DGetAffineTransform(value); - return Transform2D( - simd_float3x3({ - simd_make_float3(at.a, at.b, 0.0), - simd_make_float3(at.c, at.d, 0.0), - simd_make_float3(at.tx, at.ty, 1.0) - }) - ); - - /*Transform2D result = Transform2D::identity(); - - result.m11 = value.m11; - result.m12 = value.m12; - result.m13 = value.m13; - result.m14 = value.m14; - - result.m21 = value.m21; - result.m22 = value.m22; - result.m23 = value.m23; - result.m24 = value.m24; - - result.m31 = value.m31; - result.m32 = value.m32; - result.m33 = value.m33; - result.m34 = value.m34; - - result.m41 = value.m41; - result.m42 = value.m42; - result.m43 = value.m43; - result.m44 = value.m44; - - return result;*/ -} - -/*Transform3D Transform3D::makeRotation(float radians) { - if (std::abs(radians) <= FLT_EPSILON) { - return Transform3D::identity(); - } - - float s = sin(radians); - float c = cos(radians); - - ::CGAffineTransform t = CGAffineTransformMake(c, s, -s, c, 0.0f, 0.0f); - return fromNativeTransform(CATransform3DMakeAffineTransform(t)); -} - -Transform3D Transform3D::rotated(float degrees) const { - return Transform3D::makeRotation(degreesToRadians(degrees)) * (*this); -} - -Transform3D Transform3D::translated(Vector2D const &translation) const { - return Transform3D::makeTranslation(translation.x, translation.y, 0.0f) * (*this); -} - -Transform3D Transform3D::scaled(Vector2D const &scale) const { - return Transform3D::makeScale(scale.x, scale.y, 1.0) * (*this); -} - -bool Transform3D::isInvertible() const { - return Transform2D(*this).isInvertible(); - //return std::abs(m11 * m22 - m12 * m21) >= 0.00000001; -} - -Transform3D Transform3D::inverted() const { - return Transform2D(*this).inverted().transform3D(); -}*/ - -bool CGRect::intersects(CGRect const &other) const { - return CGRectIntersectsRect(CGRectMake(x, y, width, height), CGRectMake(other.x, other.y, other.width, other.height)); -} - -bool CGRect::contains(CGRect const &other) const { - return CGRectContainsRect(CGRectMake(x, y, width, height), CGRectMake(other.x, other.y, other.width, other.height)); -} - -CGRect CGRect::intersection(CGRect const &other) const { - auto result = CGRectIntersection(CGRectMake(x, y, width, height), CGRectMake(other.x, other.y, other.width, other.height)); - return CGRect(result.origin.x, result.origin.y, result.size.width, result.size.height); -} - -CGRect CGRect::unionWith(CGRect const &other) const { - auto result = CGRectUnion(CGRectMake(x, y, width, height), CGRectMake(other.x, other.y, other.width, other.height)); - return CGRect(result.origin.x, result.origin.y, result.size.width, result.size.height); -} - -CGRect CGRect::applyingTransform(Transform2D const &transform) const { - if (transform.isIdentity()) { - return *this; - } - - Vector2D sourceTopLeft = Vector2D(x, y); - Vector2D sourceTopRight = Vector2D(x + width, y); - Vector2D sourceBottomLeft = Vector2D(x, y + height); - Vector2D sourceBottomRight = Vector2D(x + width, y + height); - - simd_float4 xs = simd_make_float4(sourceTopLeft.x, sourceTopRight.x, sourceBottomLeft.x, sourceBottomRight.x); - simd_float4 ys = simd_make_float4(sourceTopLeft.y, sourceTopRight.y, sourceBottomLeft.y, sourceBottomRight.y); - - simd_float4 rx = xs * transform.rows().columns[0][0] + ys * transform.rows().columns[1][0] + transform.rows().columns[2][0]; - simd_float4 ry = xs * transform.rows().columns[0][1] + ys * transform.rows().columns[1][1] + transform.rows().columns[2][1]; - - Vector2D topLeft = Vector2D(rx[0], ry[0]); - Vector2D topRight = Vector2D(rx[1], ry[1]); - Vector2D bottomLeft = Vector2D(rx[2], ry[2]); - Vector2D bottomRight = Vector2D(rx[3], ry[3]); - - float minX = simd_reduce_min(simd_make_float4(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x)); - float minY = simd_reduce_min(simd_make_float4(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y)); - float maxX = simd_reduce_max(simd_make_float4(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x)); - float maxY = simd_reduce_max(simd_make_float4(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y)); - - return CGRect(minX, minY, maxX - minX, maxY - minY); -} - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/TextProvider/AnimationTextProvider.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/TextProvider/AnimationTextProvider.cpp deleted file mode 100644 index 6ba1e020c87..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/TextProvider/AnimationTextProvider.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "AnimationTextProvider.hpp" - -namespace lottie { - -} diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/TextProvider/AnimationTextProvider.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/TextProvider/AnimationTextProvider.hpp deleted file mode 100644 index dc5d3246283..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/TextProvider/AnimationTextProvider.hpp +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef AnimationTextProvider_hpp -#define AnimationTextProvider_hpp - -#include -#include - -namespace lottie { - -/// Text provider is a protocol that is used to supply text to `AnimationView`. -class AnimationTextProvider { -public: - virtual std::string textFor(std::string const &keypathName, std::string const &sourceText) = 0; -}; - -/// Text provider that simply map values from dictionary -class DictionaryTextProvider: public AnimationTextProvider { -public: - DictionaryTextProvider(std::map const &values) : - _values(values) { - } - - virtual std::string textFor(std::string const &keypathName, std::string const &sourceText) override { - const auto it = _values.find(keypathName); - if (it != _values.end()) { - return it->second; - } else { - return sourceText; - } - } - -private: - std::map _values; -}; - -/// Default text provider. Uses text in the animation file -class DefaultTextProvider: public AnimationTextProvider { -public: - DefaultTextProvider() { - } - - virtual ~DefaultTextProvider() = default; - - virtual std::string textFor(std::string const &keypathName, std::string const &sourceText) override { - return sourceText; - } -}; - -} - -#endif /* AnimationTextProvider_hpp */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimation.mm b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimation.mm deleted file mode 100644 index db68b683708..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimation.mm +++ /dev/null @@ -1,60 +0,0 @@ -#include - -#include "Lottie/Private/Model/Animation.hpp" - -#include - -@interface LottieAnimation () { -@public - std::shared_ptr _animation; -} - -@end - -@implementation LottieAnimation - -- (instancetype _Nullable)initWithData:(NSData * _Nonnull)data { - self = [super init]; - if (self != nil) { - std::string errorText; - auto json = lottiejson11::Json::parse(std::string((uint8_t const *)data.bytes, ((uint8_t const *)data.bytes) + data.length), errorText); - if (!json.is_object()) { - return nil; - } - - try { - _animation = lottie::Animation::fromJson(json.object_items()); - } catch(...) { - return nil; - } - } - return self; -} - -- (NSInteger)frameCount { - return (NSInteger)(_animation->endFrame - _animation->startFrame); -} - -- (NSInteger)framesPerSecond { - return (NSInteger)(_animation->framerate); -} - -- (CGSize)size { - return CGSizeMake(_animation->width, _animation->height); -} - -- (NSData * _Nonnull)toJson { - lottiejson11::Json::object json = _animation->toJson(); - std::string jsonString = lottiejson11::Json(json).dump(); - return [[NSData alloc] initWithBytes:jsonString.data() length:jsonString.size()]; -} - -@end - -@implementation LottieAnimation (Internal) - -- (std::shared_ptr)animationImpl { - return _animation; -} - -@end diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm deleted file mode 100644 index 5f06c012229..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm +++ /dev/null @@ -1,85 +0,0 @@ -#include - -#include "Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.hpp" -#include "LottieAnimationInternal.h" -#include - -@interface LottieAnimationContainer () { -@public - std::shared_ptr _layer; - std::shared_ptr _bezierPathsBoundingBoxContext; -} - -@end - -@implementation LottieAnimationContainer - -- (instancetype _Nonnull)initWithAnimation:(LottieAnimation * _Nonnull)animation { - self = [super init]; - if (self != nil) { - _bezierPathsBoundingBoxContext = std::make_shared(); - - _animation = animation; - - _layer = std::make_shared( - *[animation animationImpl].get(), - std::make_shared(), - std::make_shared(), - std::make_shared() - ); - } - return self; -} - -- (void)update:(NSInteger)frame { - _layer->setCurrentFrame(frame); -} - -- (LottieRenderNode * _Nullable)getCurrentRenderTreeForSize:(CGSize)size { - return nil; -} - -- (std::shared_ptr)internalGetRootRenderTreeNode { - auto renderNode = _layer->renderTreeNode(); - return renderNode; -} - -- (int64_t)getRootRenderNodeProxy { - std::shared_ptr renderNode = [self internalGetRootRenderTreeNode]; - return (int64_t)renderNode.get(); -} - -- (LottieRenderNodeProxy)getRenderNodeProxyById:(int64_t)nodeId __attribute__((objc_direct)) { - lottie::RenderTreeNode *node = (lottie::RenderTreeNode *)nodeId; - - LottieRenderNodeProxy result; - - result.internalId = nodeId; - result.isValid = node->renderData.isValid; - - - result.isInvertedMatte = node->renderData.isInvertedMatte; - if (node->mask()) { - result.maskId = (int64_t)node->mask().get(); - } else { - result.maskId = 0; - } - result.subnodeCount = (int)node->subnodes().size(); - - return result; -} - -- (LottieRenderNodeProxy)getRenderNodeSubnodeProxyById:(int64_t)nodeId index:(int)index __attribute__((objc_direct)) { - lottie::RenderTreeNode *node = (lottie::RenderTreeNode *)nodeId; - return [self getRenderNodeProxyById:(int64_t)node->subnodes()[index].get()]; -} - -@end - -@implementation LottieAnimationContainer (Internal) - -- (std::shared_ptr)layer { - return _layer; -} - -@end diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainerInternal.h b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainerInternal.h deleted file mode 100644 index c5f3a105ccd..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainerInternal.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef LottieAnimationContainerInternal_h -#define LottieAnimationContainerInternal_h - -#include "Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.hpp" -#include - -@interface LottieAnimationContainer (Internal) - -@property (nonatomic, readonly) std::shared_ptr layer; - -@end - -#endif /* LottieAnimationContainerInternal_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationInternal.h b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationInternal.h deleted file mode 100644 index d314f023d59..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationInternal.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef LottieAnimationInternal_h -#define LottieAnimationInternal_h - -#include -#include "Lottie/Private/Model/Animation.hpp" - -#include - -@interface LottieAnimation (Internal) - -- (std::shared_ptr)animationImpl; - -@end - -#endif /* LottieAnimationInternal_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.h b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.h deleted file mode 100644 index e4158c3ccbe..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.h +++ /dev/null @@ -1,147 +0,0 @@ -#ifndef LottieRenderTree_h -#define LottieRenderTree_h - -#import - -#ifdef __cplusplus -extern "C" { -#endif - -typedef NS_ENUM(NSUInteger, LottiePathItemType) { - LottiePathItemTypeMoveTo, - LottiePathItemTypeLineTo, - LottiePathItemTypeCurveTo, - LottiePathItemTypeClose -}; - -typedef struct { - LottiePathItemType type; - CGPoint points[4]; -} LottiePathItem; - -typedef struct { - CGFloat r; - CGFloat g; - CGFloat b; - CGFloat a; -} LottieColor; - -typedef NS_ENUM(NSUInteger, LottieFillRule) { - LottieFillRuleEvenOdd, - LottieFillRuleWinding -}; - -typedef NS_ENUM(NSUInteger, LottieGradientType) { - LottieGradientTypeLinear, - LottieGradientTypeRadial -}; - -@interface LottieColorStop : NSObject - -@property (nonatomic, readonly, direct) LottieColor color; -@property (nonatomic, readonly, direct) CGFloat location; - -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithColor:(LottieColor)color location:(CGFloat)location __attribute__((objc_direct)); - -@end - -@interface LottiePath : NSObject - -- (void)enumerateItems:(void (^ _Nonnull)(LottiePathItem * _Nonnull))iterate __attribute__((objc_direct)); - -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithCustomData:(NSData * _Nonnull)customData __attribute__((objc_direct)); - -@end - -@interface LottieRenderContentShading : NSObject - -@end - -@interface LottieRenderContentSolidShading : LottieRenderContentShading - -@property (nonatomic, readonly, direct) LottieColor color; -@property (nonatomic, readonly, direct) CGFloat opacity; - -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithColor:(LottieColor)color opacity:(CGFloat)opacity __attribute__((objc_direct)); - -@end - -@interface LottieRenderContentGradientShading : LottieRenderContentShading - -@property (nonatomic, readonly, direct) CGFloat opacity; -@property (nonatomic, readonly, direct) LottieGradientType gradientType; -@property (nonatomic, strong, readonly, direct) NSArray * _Nonnull colorStops; -@property (nonatomic, readonly, direct) CGPoint start; -@property (nonatomic, readonly, direct) CGPoint end; - -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithOpacity:(CGFloat)opacity gradientType:(LottieGradientType)gradientType colorStops:(NSArray * _Nonnull)colorStops start:(CGPoint)start end:(CGPoint)end __attribute__((objc_direct)); - -@end - -@interface LottieRenderContentFill : NSObject - -@property (nonatomic, strong, readonly, direct) LottieRenderContentShading * _Nonnull shading; -@property (nonatomic, readonly, direct) LottieFillRule fillRule; - -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading fillRule:(LottieFillRule)fillRule __attribute__((objc_direct)); - -@end - -@interface LottieRenderContentStroke : NSObject - -@property (nonatomic, strong, readonly, direct) LottieRenderContentShading * _Nonnull shading; -@property (nonatomic, readonly, direct) CGFloat lineWidth; -@property (nonatomic, readonly, direct) CGLineJoin lineJoin; -@property (nonatomic, readonly, direct) CGLineCap lineCap; -@property (nonatomic, readonly, direct) CGFloat miterLimit; -@property (nonatomic, readonly, direct) CGFloat dashPhase; -@property (nonatomic, strong, readonly, direct) NSArray * _Nullable dashPattern; - -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading lineWidth:(CGFloat)lineWidth lineJoin:(CGLineJoin)lineJoin lineCap:(CGLineCap)lineCap miterLimit:(CGFloat)miterLimit dashPhase:(CGFloat)dashPhase dashPattern:(NSArray * _Nullable)dashPattern __attribute__((objc_direct)); - -@end - -@interface LottieRenderContent : NSObject - -@property (nonatomic, strong, readonly, direct) LottiePath * _Nonnull path; -@property (nonatomic, strong, readonly, direct) LottieRenderContentStroke * _Nullable stroke; -@property (nonatomic, strong, readonly, direct) LottieRenderContentFill * _Nullable fill; - -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithPath:(LottiePath * _Nonnull)path stroke:(LottieRenderContentStroke * _Nullable)stroke fill:(LottieRenderContentFill * _Nullable)fill __attribute__((objc_direct)); - -@end - -@interface LottieRenderNode : NSObject - -@property (nonatomic, readonly, direct) CGPoint position; -@property (nonatomic, readonly, direct) CGRect bounds; -@property (nonatomic, readonly, direct) CATransform3D transform; -@property (nonatomic, readonly, direct) CGFloat opacity; -@property (nonatomic, readonly, direct) bool masksToBounds; -@property (nonatomic, readonly, direct) bool isHidden; - -@property (nonatomic, readonly, direct) CGRect globalRect; -@property (nonatomic, readonly, direct) CATransform3D globalTransform; -@property (nonatomic, readonly, direct) LottieRenderContent * _Nullable renderContent; -@property (nonatomic, readonly, direct) bool hasSimpleContents; -@property (nonatomic, readonly, direct) bool isInvertedMatte; -@property (nonatomic, readonly, direct) NSArray * _Nonnull subnodes; -@property (nonatomic, readonly, direct) LottieRenderNode * _Nullable mask; - -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithPosition:(CGPoint)position bounds:(CGRect)bounds transform:(CATransform3D)transform opacity:(CGFloat)opacity masksToBounds:(bool)masksToBounds isHidden:(bool)isHidden globalRect:(CGRect)globalRect globalTransform:(CATransform3D)globalTransform renderContent:(LottieRenderContent * _Nullable)renderContent hasSimpleContents:(bool)hasSimpleContents isInvertedMatte:(bool)isInvertedMatte subnodes:(NSArray * _Nonnull)subnodes mask:(LottieRenderNode * _Nullable)mask __attribute__((objc_direct)); - -@end - -#ifdef __cplusplus -} -#endif - -#endif /* LottieRenderTree_h */ diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.mm b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.mm deleted file mode 100644 index 9cdf5122133..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieRenderTree.mm +++ /dev/null @@ -1,426 +0,0 @@ -#include "LottieRenderTree.h" - -#include -#include -#import -#include "Lottie/Public/Primitives/CALayer.hpp" -#include - -namespace { - -} - -@interface LottiePath () { - std::vector _paths; - NSData *_customData; -} - -@end - -@implementation LottiePath - -- (instancetype)initWithPaths:(std::vector)paths __attribute__((objc_direct)) { - self = [super init]; - if (self != nil) { - _paths = paths; - } - return self; -} - -- (instancetype _Nonnull)initWithCustomData:(NSData * _Nonnull)customData __attribute__((objc_direct)) { - self = [super init]; - if (self != nil) { - _customData = customData; - } - return self; -} - -- (void)enumerateItems:(void (^ _Nonnull)(LottiePathItem * _Nonnull))iterate { - LottiePathItem item; - - if (_customData != nil) { - int dataOffset = 0; - int dataLength = (int)_customData.length; - uint8_t const *dataBytes = (uint8_t const *)_customData.bytes; - while (dataOffset < dataLength) { - uint8_t itemType = dataBytes[dataOffset]; - dataOffset += 1; - - switch (itemType) { - case 0: { - Float32 px; - memcpy(&px, dataBytes + dataOffset, 4); - dataOffset += 4; - - Float32 py; - memcpy(&py, dataBytes + dataOffset, 4); - dataOffset += 4; - - item.type = LottiePathItemTypeMoveTo; - item.points[0] = CGPointMake(px, py); - iterate(&item); - - break; - } - case 1: { - Float32 px; - memcpy(&px, dataBytes + dataOffset, 4); - dataOffset += 4; - - Float32 py; - memcpy(&py, dataBytes + dataOffset, 4); - dataOffset += 4; - - item.type = LottiePathItemTypeLineTo; - item.points[0] = CGPointMake(px, py); - iterate(&item); - - break; - } - case 2: { - Float32 p1x; - memcpy(&p1x, dataBytes + dataOffset, 4); - dataOffset += 4; - - Float32 p1y; - memcpy(&p1y, dataBytes + dataOffset, 4); - dataOffset += 4; - - Float32 p2x; - memcpy(&p2x, dataBytes + dataOffset, 4); - dataOffset += 4; - - Float32 p2y; - memcpy(&p2y, dataBytes + dataOffset, 4); - dataOffset += 4; - - Float32 px; - memcpy(&px, dataBytes + dataOffset, 4); - dataOffset += 4; - - Float32 py; - memcpy(&py, dataBytes + dataOffset, 4); - dataOffset += 4; - - item.type = LottiePathItemTypeCurveTo; - item.points[0] = CGPointMake(p1x, p1y); - item.points[1] = CGPointMake(p2x, p2y); - item.points[2] = CGPointMake(px, py); - iterate(&item); - - break; - } - case 3: { - item.type = LottiePathItemTypeClose; - iterate(&item); - break; - } - default: { - break; - } - } - } - } else { - for (const auto &path : _paths) { - std::optional previousElement; - for (const auto &element : path.elements()) { - if (previousElement.has_value()) { - if (previousElement->vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) { - item.type = LottiePathItemTypeLineTo; - item.points[0] = CGPointMake(element.vertex.point.x, element.vertex.point.y); - iterate(&item); - } else { - item.type = LottiePathItemTypeCurveTo; - item.points[2] = CGPointMake(element.vertex.point.x, element.vertex.point.y); - item.points[1] = CGPointMake(element.vertex.inTangent.x, element.vertex.inTangent.y); - item.points[0] = CGPointMake(previousElement->vertex.outTangent.x, previousElement->vertex.outTangent.y); - iterate(&item); - } - } else { - item.type = LottiePathItemTypeMoveTo; - item.points[0] = CGPointMake(element.vertex.point.x, element.vertex.point.y); - iterate(&item); - } - previousElement = element; - } - if (path.closed().value_or(true)) { - item.type = LottiePathItemTypeClose; - iterate(&item); - } - } - } -} - -@end - -@implementation LottieColorStop : NSObject - -- (instancetype _Nonnull)initWithColor:(LottieColor)color location:(CGFloat)location __attribute__((objc_direct)) { - self = [super init]; - if (self != nil) { - _color = color; - _location = location; - } - return self; -} - -@end - -@implementation LottieRenderContentShading - -- (instancetype _Nonnull)init { - self = [super init]; - if (self != nil) { - } - return self; -} - -@end - -static LottieColor lottieColorFromColor(lottie::Color color) { - LottieColor result; - result.r = color.r; - result.g = color.g; - result.b = color.b; - result.a = color.a; - - return result; -} - -@implementation LottieRenderContentSolidShading - -- (instancetype _Nonnull)initWithSolidShading:(lottie::RenderTreeNodeContentItem::SolidShading *)solidShading __attribute__((objc_direct)) { - self = [super init]; - if (self != nil) { - _color = lottieColorFromColor(solidShading->color); - _opacity = solidShading->opacity; - } - return self; -} - -- (instancetype _Nonnull)initWithColor:(LottieColor)color opacity:(CGFloat)opacity { - self = [super init]; - if (self != nil) { - _color = color; - _opacity = opacity; - } - return self; -} - -@end - -@implementation LottieRenderContentGradientShading - -- (instancetype _Nonnull)initWithGradientShading:(lottie::RenderTreeNodeContentItem::GradientShading *)gradientShading __attribute__((objc_direct)) { - self = [super init]; - if (self != nil) { - _opacity = gradientShading->opacity; - - switch (gradientShading->gradientType) { - case lottie::GradientType::Radial: { - _gradientType = LottieGradientTypeRadial; - break; - } - default: { - _gradientType = LottieGradientTypeLinear; - break; - } - } - - NSMutableArray *colorStops = [[NSMutableArray alloc] initWithCapacity:gradientShading->colors.size()]; - for (size_t i = 0; i < gradientShading->colors.size(); i++) { - [colorStops addObject:[[LottieColorStop alloc] initWithColor:lottieColorFromColor(gradientShading->colors[i]) location:gradientShading->locations[i]]]; - } - _colorStops = colorStops; - - _start = CGPointMake(gradientShading->start.x, gradientShading->start.y); - _end = CGPointMake(gradientShading->end.x, gradientShading->end.y); - } - return self; -} - -- (instancetype _Nonnull)initWithOpacity:(CGFloat)opacity gradientType:(LottieGradientType)gradientType colorStops:(NSArray * _Nonnull)colorStops start:(CGPoint)start end:(CGPoint)end __attribute__((objc_direct)) { - self = [super init]; - if (self != nil) { - _opacity = opacity; - _gradientType = gradientType; - _colorStops = colorStops; - _start = start; - _end = end; - } - return self; -} - -@end - -@implementation LottieRenderContentFill - -- (instancetype _Nonnull)initWithFill:(std::shared_ptr const &)fill __attribute__((objc_direct)) { - self = [super init]; - if (self != nil) { - switch (fill->shading->type()) { - case lottie::RenderTreeNodeContentItem::ShadingType::Solid: { - _shading = [[LottieRenderContentSolidShading alloc] initWithSolidShading:(lottie::RenderTreeNodeContentItem::SolidShading *)fill->shading.get()]; - break; - } - case lottie::RenderTreeNodeContentItem::ShadingType::Gradient: { - _shading = [[LottieRenderContentGradientShading alloc] initWithGradientShading:(lottie::RenderTreeNodeContentItem::GradientShading *)fill->shading.get()]; - break; - } - default: { - abort(); - } - } - - switch (fill->rule) { - case lottie::FillRule::EvenOdd: { - _fillRule = LottieFillRuleEvenOdd; - break; - } - default: { - _fillRule = LottieFillRuleWinding; - break; - } - } - } - return self; -} - -- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading fillRule:(LottieFillRule)fillRule __attribute__((objc_direct)) { - self = [super init]; - if (self != nil) { - _shading = shading; - _fillRule = fillRule; - } - return self; -} - -@end - -@implementation LottieRenderContentStroke - -- (instancetype _Nonnull)initWithStroke:(std::shared_ptr const &)stroke __attribute__((objc_direct)) { - self = [super init]; - if (self != nil) { - switch (stroke->shading->type()) { - case lottie::RenderTreeNodeContentItem::ShadingType::Solid: { - _shading = [[LottieRenderContentSolidShading alloc] initWithSolidShading:(lottie::RenderTreeNodeContentItem::SolidShading *)stroke->shading.get()]; - break; - } - case lottie::RenderTreeNodeContentItem::ShadingType::Gradient: { - _shading = [[LottieRenderContentGradientShading alloc] initWithGradientShading:(lottie::RenderTreeNodeContentItem::GradientShading *)stroke->shading.get()]; - break; - } - default: { - abort(); - } - } - - _lineWidth = stroke->lineWidth; - - switch (stroke->lineJoin) { - case lottie::LineJoin::Miter: { - _lineJoin = kCGLineJoinMiter; - break; - } - case lottie::LineJoin::Round: { - _lineJoin = kCGLineJoinRound; - break; - } - case lottie::LineJoin::Bevel: { - _lineJoin = kCGLineJoinBevel; - break; - } - default: { - _lineJoin = kCGLineJoinBevel; - break; - } - } - - switch (stroke->lineCap) { - case lottie::LineCap::Butt: { - _lineCap = kCGLineCapButt; - break; - } - case lottie::LineCap::Round: { - _lineCap = kCGLineCapRound; - break; - } - case lottie::LineCap::Square: { - _lineCap = kCGLineCapSquare; - break; - } - default: { - _lineCap = kCGLineCapSquare; - break; - } - } - - _miterLimit = stroke->miterLimit; - - _dashPhase = stroke->dashPhase; - - if (!stroke->dashPattern.empty()) { - NSMutableArray *dashPattern = [[NSMutableArray alloc] initWithCapacity:stroke->dashPattern.size()]; - for (auto value : stroke->dashPattern) { - [dashPattern addObject:@(value)]; - } - _dashPattern = dashPattern; - } - } - return self; -} - -- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading lineWidth:(CGFloat)lineWidth lineJoin:(CGLineJoin)lineJoin lineCap:(CGLineCap)lineCap miterLimit:(CGFloat)miterLimit dashPhase:(CGFloat)dashPhase dashPattern:(NSArray * _Nullable)dashPattern __attribute__((objc_direct)) { - self = [super init]; - if (self != nil) { - _shading = shading; - _lineWidth = lineWidth; - _lineJoin = lineJoin; - _lineCap = lineCap; - _miterLimit = miterLimit; - _dashPhase = dashPhase; - _dashPattern = dashPattern; - } - return self; -} - -@end - -@implementation LottieRenderContent - -- (instancetype _Nonnull)initWithPath:(LottiePath * _Nonnull)path stroke:(LottieRenderContentStroke * _Nullable)stroke fill:(LottieRenderContentFill * _Nullable)fill __attribute__((objc_direct)) { - self = [super init]; - if (self != nil) { - _path = path; - _stroke = stroke; - _fill = fill; - } - return self; -} - -@end - -@implementation LottieRenderNode - -- (instancetype _Nonnull)initWithPosition:(CGPoint)position bounds:(CGRect)bounds transform:(CATransform3D)transform opacity:(CGFloat)opacity masksToBounds:(bool)masksToBounds isHidden:(bool)isHidden globalRect:(CGRect)globalRect globalTransform:(CATransform3D)globalTransform renderContent:(LottieRenderContent * _Nullable)renderContent hasSimpleContents:(bool)hasSimpleContents isInvertedMatte:(bool)isInvertedMatte subnodes:(NSArray * _Nonnull)subnodes mask:(LottieRenderNode * _Nullable)mask __attribute__((objc_direct)) { - self = [super init]; - if (self != nil) { - _position = position; - _bounds = bounds; - _transform = transform; - _opacity = opacity; - _masksToBounds = masksToBounds; - _isHidden = isHidden; - _globalRect = globalRect; - _globalTransform= globalTransform; - _renderContent = renderContent; - _hasSimpleContents = hasSimpleContents; - _isInvertedMatte = isInvertedMatte; - _subnodes = subnodes; - _mask = mask; - } - return self; -} - -@end diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/lottiejson11/lottiejson11.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/lottiejson11/lottiejson11.cpp deleted file mode 100644 index f5a69b64e8a..00000000000 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/lottiejson11/lottiejson11.cpp +++ /dev/null @@ -1,790 +0,0 @@ -/* Copyright (c) 2013 Dropbox, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -#include -#include -#include -#include -#include -#include - -namespace lottiejson11 { - -static const int max_depth = 200; - -using std::string; -using std::vector; -using std::map; -using std::make_shared; -using std::initializer_list; -using std::move; - -/* Helper for representing null - just a do-nothing struct, plus comparison - * operators so the helpers in JsonValue work. We can't use nullptr_t because - * it may not be orderable. - */ -struct NullStruct { - bool operator==(NullStruct) const { return true; } - bool operator<(NullStruct) const { return false; } -}; - -/* * * * * * * * * * * * * * * * * * * * - * Serialization - */ - -static void dump(NullStruct, string &out) { - out += "null"; -} - -static void dump(double value, string &out) { - if (std::isfinite(value)) { - char buf[32]; - snprintf(buf, sizeof buf, "%.17g", value); - out += buf; - } else { - out += "null"; - } -} - -static void dump(int value, string &out) { - char buf[32]; - snprintf(buf, sizeof buf, "%d", value); - out += buf; -} - -static void dump(bool value, string &out) { - out += value ? "true" : "false"; -} - -static void dump(const string &value, string &out) { - out += '"'; - for (size_t i = 0; i < value.length(); i++) { - const char ch = value[i]; - if (ch == '\\') { - out += "\\\\"; - } else if (ch == '"') { - out += "\\\""; - } else if (ch == '\b') { - out += "\\b"; - } else if (ch == '\f') { - out += "\\f"; - } else if (ch == '\n') { - out += "\\n"; - } else if (ch == '\r') { - out += "\\r"; - } else if (ch == '\t') { - out += "\\t"; - } else if (static_cast(ch) <= 0x1f) { - char buf[8]; - snprintf(buf, sizeof buf, "\\u%04x", ch); - out += buf; - } else if (static_cast(ch) == 0xe2 && static_cast(value[i+1]) == 0x80 - && static_cast(value[i+2]) == 0xa8) { - out += "\\u2028"; - i += 2; - } else if (static_cast(ch) == 0xe2 && static_cast(value[i+1]) == 0x80 - && static_cast(value[i+2]) == 0xa9) { - out += "\\u2029"; - i += 2; - } else { - out += ch; - } - } - out += '"'; -} - -static void dump(const Json::array &values, string &out) { - bool first = true; - out += "["; - for (const auto &value : values) { - if (!first) - out += ", "; - value.dump(out); - first = false; - } - out += "]"; -} - -static void dump(const Json::object &values, string &out) { - bool first = true; - out += "{"; - for (const auto &kv : values) { - if (!first) - out += ", "; - dump(kv.first, out); - out += ": "; - kv.second.dump(out); - first = false; - } - out += "}"; -} - -void Json::dump(string &out) const { - m_ptr->dump(out); -} - -/* * * * * * * * * * * * * * * * * * * * - * Value wrappers - */ - -template -class Value : public JsonValue { -protected: - - // Constructors - explicit Value(const T &value) : m_value(value) {} - explicit Value(T &&value) : m_value(std::move(value)) {} - - // Get type tag - Json::Type type() const override { - return tag; - } - - // Comparisons - bool equals(const JsonValue * other) const override { - return m_value == static_cast *>(other)->m_value; - } - bool less(const JsonValue * other) const override { - return m_value < static_cast *>(other)->m_value; - } - - const T m_value; - void dump(string &out) const override { lottiejson11::dump(m_value, out); } -}; - -class JsonDouble final : public Value { - double number_value() const override { return m_value; } - int int_value() const override { return static_cast(m_value); } - bool equals(const JsonValue * other) const override { return m_value == other->number_value(); } - bool less(const JsonValue * other) const override { return m_value < other->number_value(); } -public: - explicit JsonDouble(double value) : Value(value) {} -}; - -class JsonInt final : public Value { - double number_value() const override { return m_value; } - int int_value() const override { return m_value; } - bool equals(const JsonValue * other) const override { return m_value == other->number_value(); } - bool less(const JsonValue * other) const override { return m_value < other->number_value(); } -public: - explicit JsonInt(int value) : Value(value) {} -}; - -class JsonBoolean final : public Value { - bool bool_value() const override { return m_value; } -public: - explicit JsonBoolean(bool value) : Value(value) {} -}; - -class JsonString final : public Value { - const string &string_value() const override { return m_value; } -public: - explicit JsonString(const string &value) : Value(value) {} - explicit JsonString(string &&value) : Value(std::move(value)) {} -}; - -class JsonArray final : public Value { - const Json::array &array_items() const override { return m_value; } - const Json & operator[](size_t i) const override; -public: - explicit JsonArray(const Json::array &value) : Value(value) {} - explicit JsonArray(Json::array &&value) : Value(std::move(value)) {} -}; - -class JsonObject final : public Value { - const Json::object &object_items() const override { return m_value; } - const Json & operator[](const string &key) const override; -public: - explicit JsonObject(const Json::object &value) : Value(value) {} - explicit JsonObject(Json::object &&value) : Value(std::move(value)) {} -}; - -class JsonNull final : public Value { -public: - JsonNull() : Value({}) {} -}; - -/* * * * * * * * * * * * * * * * * * * * - * Static globals - static-init-safe - */ -struct Statics { - const std::shared_ptr null = make_shared(); - const std::shared_ptr t = make_shared(true); - const std::shared_ptr f = make_shared(false); - const string empty_string; - const vector empty_vector; - const map empty_map; - Statics() {} -}; - -static const Statics & statics() { - static const Statics s {}; - return s; -} - -static const Json & static_null() { - // This has to be separate, not in Statics, because Json() accesses statics().null. - static const Json json_null; - return json_null; -} - -/* * * * * * * * * * * * * * * * * * * * - * Constructors - */ - -Json::Json() noexcept : m_ptr(statics().null) {} -Json::Json(std::nullptr_t) noexcept : m_ptr(statics().null) {} -Json::Json(double value) : m_ptr(make_shared(value)) {} -Json::Json(int value) : m_ptr(make_shared(value)) {} -Json::Json(bool value) : m_ptr(value ? statics().t : statics().f) {} -Json::Json(const string &value) : m_ptr(make_shared(value)) {} -Json::Json(string &&value) : m_ptr(make_shared(std::move(value))) {} -Json::Json(const char * value) : m_ptr(make_shared(value)) {} -Json::Json(const Json::array &values) : m_ptr(make_shared(values)) {} -Json::Json(Json::array &&values) : m_ptr(make_shared(std::move(values))) {} -Json::Json(const Json::object &values) : m_ptr(make_shared(values)) {} -Json::Json(Json::object &&values) : m_ptr(make_shared(std::move(values))) {} - -/* * * * * * * * * * * * * * * * * * * * - * Accessors - */ - -Json::Type Json::type() const { return m_ptr->type(); } -double Json::number_value() const { return m_ptr->number_value(); } -int Json::int_value() const { return m_ptr->int_value(); } -bool Json::bool_value() const { return m_ptr->bool_value(); } -const string & Json::string_value() const { return m_ptr->string_value(); } -const vector & Json::array_items() const { return m_ptr->array_items(); } -const map & Json::object_items() const { return m_ptr->object_items(); } -const Json & Json::operator[] (size_t i) const { return (*m_ptr)[i]; } -const Json & Json::operator[] (const string &key) const { return (*m_ptr)[key]; } - -double JsonValue::number_value() const { return 0; } -int JsonValue::int_value() const { return 0; } -bool JsonValue::bool_value() const { return false; } -const string & JsonValue::string_value() const { return statics().empty_string; } -const vector & JsonValue::array_items() const { return statics().empty_vector; } -const map & JsonValue::object_items() const { return statics().empty_map; } -const Json & JsonValue::operator[] (size_t) const { return static_null(); } -const Json & JsonValue::operator[] (const string &) const { return static_null(); } - -const Json & JsonObject::operator[] (const string &key) const { - auto iter = m_value.find(key); - return (iter == m_value.end()) ? static_null() : iter->second; -} -const Json & JsonArray::operator[] (size_t i) const { - if (i >= m_value.size()) return static_null(); - else return m_value[i]; -} - -/* * * * * * * * * * * * * * * * * * * * - * Comparison - */ - -bool Json::operator== (const Json &other) const { - if (m_ptr == other.m_ptr) - return true; - if (m_ptr->type() != other.m_ptr->type()) - return false; - - return m_ptr->equals(other.m_ptr.get()); -} - -bool Json::operator< (const Json &other) const { - if (m_ptr == other.m_ptr) - return false; - if (m_ptr->type() != other.m_ptr->type()) - return m_ptr->type() < other.m_ptr->type(); - - return m_ptr->less(other.m_ptr.get()); -} - -/* * * * * * * * * * * * * * * * * * * * - * Parsing - */ - -/* esc(c) - * - * Format char c suitable for printing in an error message. - */ -static inline string esc(char c) { - char buf[12]; - if (static_cast(c) >= 0x20 && static_cast(c) <= 0x7f) { - snprintf(buf, sizeof buf, "'%c' (%d)", c, c); - } else { - snprintf(buf, sizeof buf, "(%d)", c); - } - return string(buf); -} - -static inline bool in_range(long x, long lower, long upper) { - return (x >= lower && x <= upper); -} - -namespace { -/* JsonParser - * - * Object that tracks all state of an in-progress parse. - */ -struct JsonParser final { - - /* State - */ - const string &str; - size_t i; - string &err; - bool failed; - const JsonParse strategy; - - /* fail(msg, err_ret = Json()) - * - * Mark this parse as failed. - */ - Json fail(string &&msg) { - return fail(std::move(msg), Json()); - } - - template - T fail(string &&msg, const T err_ret) { - if (!failed) - err = std::move(msg); - failed = true; - return err_ret; - } - - /* consume_whitespace() - * - * Advance until the current character is non-whitespace. - */ - void consume_whitespace() { - while (str[i] == ' ' || str[i] == '\r' || str[i] == '\n' || str[i] == '\t') - i++; - } - - /* consume_comment() - * - * Advance comments (c-style inline and multiline). - */ - bool consume_comment() { - bool comment_found = false; - if (str[i] == '/') { - i++; - if (i == str.size()) - return fail("unexpected end of input after start of comment", false); - if (str[i] == '/') { // inline comment - i++; - // advance until next line, or end of input - while (i < str.size() && str[i] != '\n') { - i++; - } - comment_found = true; - } - else if (str[i] == '*') { // multiline comment - i++; - if (i > str.size()-2) - return fail("unexpected end of input inside multi-line comment", false); - // advance until closing tokens - while (!(str[i] == '*' && str[i+1] == '/')) { - i++; - if (i > str.size()-2) - return fail( - "unexpected end of input inside multi-line comment", false); - } - i += 2; - comment_found = true; - } - else - return fail("malformed comment", false); - } - return comment_found; - } - - /* consume_garbage() - * - * Advance until the current character is non-whitespace and non-comment. - */ - void consume_garbage() { - consume_whitespace(); - if(strategy == JsonParse::COMMENTS) { - bool comment_found = false; - do { - comment_found = consume_comment(); - if (failed) return; - consume_whitespace(); - } - while(comment_found); - } - } - - /* get_next_token() - * - * Return the next non-whitespace character. If the end of the input is reached, - * flag an error and return 0. - */ - char get_next_token() { - consume_garbage(); - if (failed) return static_cast(0); - if (i == str.size()) - return fail("unexpected end of input", static_cast(0)); - - return str[i++]; - } - - /* encode_utf8(pt, out) - * - * Encode pt as UTF-8 and add it to out. - */ - void encode_utf8(long pt, string & out) { - if (pt < 0) - return; - - if (pt < 0x80) { - out += static_cast(pt); - } else if (pt < 0x800) { - out += static_cast((pt >> 6) | 0xC0); - out += static_cast((pt & 0x3F) | 0x80); - } else if (pt < 0x10000) { - out += static_cast((pt >> 12) | 0xE0); - out += static_cast(((pt >> 6) & 0x3F) | 0x80); - out += static_cast((pt & 0x3F) | 0x80); - } else { - out += static_cast((pt >> 18) | 0xF0); - out += static_cast(((pt >> 12) & 0x3F) | 0x80); - out += static_cast(((pt >> 6) & 0x3F) | 0x80); - out += static_cast((pt & 0x3F) | 0x80); - } - } - - /* parse_string() - * - * Parse a string, starting at the current position. - */ - string parse_string() { - string out; - long last_escaped_codepoint = -1; - while (true) { - if (i == str.size()) - return fail("unexpected end of input in string", ""); - - char ch = str[i++]; - - if (ch == '"') { - encode_utf8(last_escaped_codepoint, out); - return out; - } - - if (in_range(ch, 0, 0x1f)) - return fail("unescaped " + esc(ch) + " in string", ""); - - // The usual case: non-escaped characters - if (ch != '\\') { - encode_utf8(last_escaped_codepoint, out); - last_escaped_codepoint = -1; - out += ch; - continue; - } - - // Handle escapes - if (i == str.size()) - return fail("unexpected end of input in string", ""); - - ch = str[i++]; - - if (ch == 'u') { - // Extract 4-byte escape sequence - string esc = str.substr(i, 4); - // Explicitly check length of the substring. The following loop - // relies on std::string returning the terminating NUL when - // accessing str[length]. Checking here reduces brittleness. - if (esc.length() < 4) { - return fail("bad \\u escape: " + esc, ""); - } - for (size_t j = 0; j < 4; j++) { - if (!in_range(esc[j], 'a', 'f') && !in_range(esc[j], 'A', 'F') - && !in_range(esc[j], '0', '9')) - return fail("bad \\u escape: " + esc, ""); - } - - long codepoint = strtol(esc.data(), nullptr, 16); - - // JSON specifies that characters outside the BMP shall be encoded as a pair - // of 4-hex-digit \u escapes encoding their surrogate pair components. Check - // whether we're in the middle of such a beast: the previous codepoint was an - // escaped lead (high) surrogate, and this is a trail (low) surrogate. - if (in_range(last_escaped_codepoint, 0xD800, 0xDBFF) - && in_range(codepoint, 0xDC00, 0xDFFF)) { - // Reassemble the two surrogate pairs into one astral-plane character, per - // the UTF-16 algorithm. - encode_utf8((((last_escaped_codepoint - 0xD800) << 10) - | (codepoint - 0xDC00)) + 0x10000, out); - last_escaped_codepoint = -1; - } else { - encode_utf8(last_escaped_codepoint, out); - last_escaped_codepoint = codepoint; - } - - i += 4; - continue; - } - - encode_utf8(last_escaped_codepoint, out); - last_escaped_codepoint = -1; - - if (ch == 'b') { - out += '\b'; - } else if (ch == 'f') { - out += '\f'; - } else if (ch == 'n') { - out += '\n'; - } else if (ch == 'r') { - out += '\r'; - } else if (ch == 't') { - out += '\t'; - } else if (ch == '"' || ch == '\\' || ch == '/') { - out += ch; - } else { - return fail("invalid escape character " + esc(ch), ""); - } - } - } - - /* parse_number() - * - * Parse a double. - */ - Json parse_number() { - size_t start_pos = i; - - if (str[i] == '-') - i++; - - // Integer part - if (str[i] == '0') { - i++; - if (in_range(str[i], '0', '9')) - return fail("leading 0s not permitted in numbers"); - } else if (in_range(str[i], '1', '9')) { - i++; - while (in_range(str[i], '0', '9')) - i++; - } else { - return fail("invalid " + esc(str[i]) + " in number"); - } - - if (str[i] != '.' && str[i] != 'e' && str[i] != 'E' - && (i - start_pos) <= static_cast(std::numeric_limits::digits10)) { - return std::atoi(str.c_str() + start_pos); - } - - // Decimal part - if (str[i] == '.') { - i++; - if (!in_range(str[i], '0', '9')) - return fail("at least one digit required in fractional part"); - - while (in_range(str[i], '0', '9')) - i++; - } - - // Exponent part - if (str[i] == 'e' || str[i] == 'E') { - i++; - - if (str[i] == '+' || str[i] == '-') - i++; - - if (!in_range(str[i], '0', '9')) - return fail("at least one digit required in exponent"); - - while (in_range(str[i], '0', '9')) - i++; - } - - return std::strtod(str.c_str() + start_pos, nullptr); - } - - /* expect(str, res) - * - * Expect that 'str' starts at the character that was just read. If it does, advance - * the input and return res. If not, flag an error. - */ - Json expect(const string &expected, Json res) { - assert(i != 0); - i--; - if (str.compare(i, expected.length(), expected) == 0) { - i += expected.length(); - return res; - } else { - return fail("parse error: expected " + expected + ", got " + str.substr(i, expected.length())); - } - } - - /* parse_json() - * - * Parse a JSON object. - */ - Json parse_json(int depth) { - if (depth > max_depth) { - return fail("exceeded maximum nesting depth"); - } - - char ch = get_next_token(); - if (failed) - return Json(); - - if (ch == '-' || (ch >= '0' && ch <= '9')) { - i--; - return parse_number(); - } - - if (ch == 't') - return expect("true", true); - - if (ch == 'f') - return expect("false", false); - - if (ch == 'n') - return expect("null", Json()); - - if (ch == '"') - return parse_string(); - - if (ch == '{') { - map data; - ch = get_next_token(); - if (ch == '}') - return data; - - while (1) { - if (ch != '"') - return fail("expected '\"' in object, got " + esc(ch)); - - string key = parse_string(); - if (failed) - return Json(); - - ch = get_next_token(); - if (ch != ':') - return fail("expected ':' in object, got " + esc(ch)); - - data[std::move(key)] = parse_json(depth + 1); - if (failed) - return Json(); - - ch = get_next_token(); - if (ch == '}') - break; - if (ch != ',') - return fail("expected ',' in object, got " + esc(ch)); - - ch = get_next_token(); - } - return data; - } - - if (ch == '[') { - vector data; - ch = get_next_token(); - if (ch == ']') - return data; - - while (1) { - i--; - data.push_back(parse_json(depth + 1)); - if (failed) - return Json(); - - ch = get_next_token(); - if (ch == ']') - break; - if (ch != ',') - return fail("expected ',' in list, got " + esc(ch)); - - ch = get_next_token(); - (void)ch; - } - return data; - } - - return fail("expected value, got " + esc(ch)); - } -}; -}//namespace { - -Json Json::parse(const string &in, string &err, JsonParse strategy) { - JsonParser parser { in, 0, err, false, strategy }; - Json result = parser.parse_json(0); - - // Check for any trailing garbage - parser.consume_garbage(); - if (parser.failed) - return Json(); - if (parser.i != in.size()) - return parser.fail("unexpected trailing " + esc(in[parser.i])); - - return result; -} - -// Documented in lottiejson11.hpp -vector Json::parse_multi(const string &in, - std::string::size_type &parser_stop_pos, - string &err, - JsonParse strategy) { - JsonParser parser { in, 0, err, false, strategy }; - parser_stop_pos = 0; - vector json_vec; - while (parser.i != in.size() && !parser.failed) { - json_vec.push_back(parser.parse_json(0)); - if (parser.failed) - break; - - // Check for another object - parser.consume_garbage(); - if (parser.failed) - break; - parser_stop_pos = parser.i; - } - return json_vec; -} - -/* * * * * * * * * * * * * * * * * * * * - * Shape-checking - */ - -bool Json::has_shape(const shape & types, string & err) const { - if (!is_object()) { - err = "expected JSON object, got " + dump(); - return false; - } - - const auto& obj_items = object_items(); - for (auto & item : types) { - const auto it = obj_items.find(item.first); - if (it == obj_items.cend() || it->second.type() != item.second) { - err = "bad type for " + item.first + " in " + dump(); - return false; - } - } - - return true; -} - -} // namespace lottiejson11 diff --git a/submodules/TelegramUI/Components/LottieMetal/BUILD b/submodules/TelegramUI/Components/LottieMetal/BUILD index dd63b1b0eb1..6b3709b2f40 100644 --- a/submodules/TelegramUI/Components/LottieMetal/BUILD +++ b/submodules/TelegramUI/Components/LottieMetal/BUILD @@ -60,7 +60,7 @@ swift_library( "//submodules/AnimatedStickerNode", "//submodules/GZip", "//submodules/MetalEngine", - "//submodules/TelegramUI/Components/LottieCpp", + "//submodules/LottieCpp", "//submodules/Components/HierarchyTrackingLayer", ], visibility = [ diff --git a/submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift b/submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift index 9e08a17d944..6d06528b45a 100644 --- a/submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift +++ b/submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift @@ -34,7 +34,7 @@ func metalLibrary(device: MTLDevice) -> MTLLibrary? { return library } -private func generateTexture(device: MTLDevice, sideSize: Int, msaaSampleCount: Int) -> MTLTexture { +/*private func generateTexture(device: MTLDevice, sideSize: Int, msaaSampleCount: Int) -> MTLTexture { let textureDescriptor = MTLTextureDescriptor() textureDescriptor.sampleCount = msaaSampleCount if msaaSampleCount == 1 { @@ -53,7 +53,7 @@ private func generateTexture(device: MTLDevice, sideSize: Int, msaaSampleCount: } public func cacheLottieMetalAnimation(path: String) -> Data? { - if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { + /*if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data if let lottieAnimation = LottieAnimation(data: decompressedData) { let animationContainer = LottieAnimationContainer(animation: lottieAnimation) @@ -92,7 +92,7 @@ public func cacheLottieMetalAnimation(path: String) -> Data? { return zippedData } - } + }*/ return nil } @@ -1030,7 +1030,7 @@ public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedSticke if let serializedFrames { content = .serialized(frameMapping: serializedFrames.0, data: serializedFrames.1) } else { - guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { + /*guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return } let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data @@ -1045,7 +1045,8 @@ public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedSticke AnimationCacheState.shared.enqueue(path: path, cachePath: cachePathValue) } - content = .animation(lottieInstance) + content = .animation(lottieInstance)*/ + return } Queue.mainQueue().async { @@ -1165,4 +1166,4 @@ public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedSticke public func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) { } -} +}*/ diff --git a/submodules/TelegramUI/Components/LottieMetal/Sources/PathFrameState.swift b/submodules/TelegramUI/Components/LottieMetal/Sources/PathFrameState.swift index 9013fff1daa..1495c3b7d58 100644 --- a/submodules/TelegramUI/Components/LottieMetal/Sources/PathFrameState.swift +++ b/submodules/TelegramUI/Components/LottieMetal/Sources/PathFrameState.swift @@ -2,7 +2,7 @@ import Foundation import MetalKit import LottieCpp -private func alignUp(size: Int, align: Int) -> Int { +/*private func alignUp(size: Int, align: Int) -> Int { precondition(((align - 1) & align) == 0, "Align must be a power of two") let alignmentMask = align - 1 @@ -372,3 +372,4 @@ final class PathFrameState { computeEncoder.dispatchThreadgroups(MTLSize(width: dispatchSize, height: 1, depth: 1), threadsPerThreadgroup: MTLSize(width: 1, height: threadGroupHeight, depth: 1)) } } +*/ diff --git a/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderFillState.swift b/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderFillState.swift index f7f22cdd56a..0140da0a9ac 100644 --- a/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderFillState.swift +++ b/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderFillState.swift @@ -3,7 +3,7 @@ import MetalKit import simd import LottieCpp -enum PathShading { +/*enum PathShading { final class Gradient { enum GradientType { case linear @@ -274,4 +274,4 @@ final class PathRenderFillState { encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: quadVertices.count) } } - +*/ diff --git a/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderStrokeState.swift b/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderStrokeState.swift index ce9accfe1fc..264e984f088 100644 --- a/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderStrokeState.swift +++ b/submodules/TelegramUI/Components/LottieMetal/Sources/PathRenderStrokeState.swift @@ -2,7 +2,7 @@ import Foundation import MetalKit import LottieCpp -func evaluateBezier(p0: SIMD2, p1: SIMD2, p2: SIMD2, p3: SIMD2, t: Float) -> SIMD2 { +/*func evaluateBezier(p0: SIMD2, p1: SIMD2, p2: SIMD2, p3: SIMD2, t: Float) -> SIMD2 { let t2 = t * t let t3 = t * t * t @@ -422,4 +422,4 @@ final class PathRenderStrokeState { } } } -} +}*/ diff --git a/submodules/TelegramUI/Components/LottieMetal/Sources/RenderTreeSerialization.swift b/submodules/TelegramUI/Components/LottieMetal/Sources/RenderTreeSerialization.swift index c52b1769869..5238b2d212b 100644 --- a/submodules/TelegramUI/Components/LottieMetal/Sources/RenderTreeSerialization.swift +++ b/submodules/TelegramUI/Components/LottieMetal/Sources/RenderTreeSerialization.swift @@ -1,519 +1,3 @@ import Foundation import LottieCpp -final class WriteBuffer { - private(set) var data: Data - private var capacity: Int - var length: Int - - init() { - self.capacity = 1024 - self.data = Data(count: self.capacity) - self.length = 0 - } - - func trim() { - self.data.count = self.length - self.capacity = self.data.count - } - - func write(bytes: UnsafeRawBufferPointer) { - if self.data.count < self.length + bytes.count { - self.data.count = self.data.count * 2 - } - self.data.withUnsafeMutableBytes { buffer -> Void in - memcpy(buffer.baseAddress!.advanced(by: self.length), bytes.baseAddress!, bytes.count) - } - self.length += bytes.count - } - - func write(uInt32 value: UInt32) { - var value = value - withUnsafeBytes(of: &value, { bytes in - self.write(bytes: bytes) - }) - } - - func write(uInt16 value: UInt16) { - var value = value - withUnsafeBytes(of: &value, { bytes in - self.write(bytes: bytes) - }) - } - - func write(uInt8 value: UInt8) { - var value = value - withUnsafeBytes(of: &value, { bytes in - self.write(bytes: bytes) - }) - } - - func write(float value: Float) { - var value = value - withUnsafeBytes(of: &value, { bytes in - self.write(bytes: bytes) - }) - } - - func write(point: CGPoint) { - self.write(float: Float(point.x)) - self.write(float: Float(point.y)) - } - - func write(size: CGSize) { - self.write(float: Float(size.width)) - self.write(float: Float(size.height)) - } - - func write(rect: CGRect) { - self.write(point: rect.origin) - self.write(size: rect.size) - } - - func write(transform: CATransform3D) { - self.write(float: Float(transform.m11)) - self.write(float: Float(transform.m12)) - self.write(float: Float(transform.m13)) - self.write(float: Float(transform.m14)) - self.write(float: Float(transform.m21)) - self.write(float: Float(transform.m22)) - self.write(float: Float(transform.m23)) - self.write(float: Float(transform.m24)) - self.write(float: Float(transform.m31)) - self.write(float: Float(transform.m32)) - self.write(float: Float(transform.m33)) - self.write(float: Float(transform.m34)) - self.write(float: Float(transform.m41)) - self.write(float: Float(transform.m42)) - self.write(float: Float(transform.m43)) - self.write(float: Float(transform.m44)) - } -} - -final class ReadBuffer { - private let data: Data - private var offset: Int - - init(data: Data) { - self.data = data - self.offset = 0 - } - - func read(bytes: UnsafeMutableRawBufferPointer) { - if self.offset + bytes.count <= self.data.count { - self.data.withUnsafeBytes { buffer -> Void in - memcpy(bytes.baseAddress!, buffer.baseAddress!.advanced(by: self.offset), bytes.count) - } - self.offset += bytes.count - } else { - preconditionFailure() - } - } - - func readUInt32() -> UInt32 { - var value: UInt32 = 0 - withUnsafeMutableBytes(of: &value, { bytes in - self.read(bytes: bytes) - }) - return value - } - - func readUInt16() -> UInt16 { - var value: UInt16 = 0 - withUnsafeMutableBytes(of: &value, { bytes in - self.read(bytes: bytes) - }) - return value - } - - func readUInt8() -> UInt8 { - var value: UInt8 = 0 - withUnsafeMutableBytes(of: &value, { bytes in - self.read(bytes: bytes) - }) - return value - } - - func readFloat() -> Float { - var value: Float = 0 - withUnsafeMutableBytes(of: &value, { bytes in - self.read(bytes: bytes) - }) - return value - } - - func readPoint() -> CGPoint { - return CGPoint(x: CGFloat(self.readFloat()), y: CGFloat(self.readFloat())) - } - - func readSize() -> CGSize { - return CGSize(width: CGFloat(self.readFloat()), height: CGFloat(self.readFloat())) - } - - func readRect() -> CGRect { - return CGRect(origin: self.readPoint(), size: self.readSize()) - } - - func readTransform() -> CATransform3D { - return CATransform3D( - m11: CGFloat(self.readFloat()), - m12: CGFloat(self.readFloat()), - m13: CGFloat(self.readFloat()), - m14: CGFloat(self.readFloat()), - m21: CGFloat(self.readFloat()), - m22: CGFloat(self.readFloat()), - m23: CGFloat(self.readFloat()), - m24: CGFloat(self.readFloat()), - m31: CGFloat(self.readFloat()), - m32: CGFloat(self.readFloat()), - m33: CGFloat(self.readFloat()), - m34: CGFloat(self.readFloat()), - m41: CGFloat(self.readFloat()), - m42: CGFloat(self.readFloat()), - m43: CGFloat(self.readFloat()), - m44: CGFloat(self.readFloat()) - ) - } -} - -private extension LottieColor { - init(argb: UInt32) { - self.init(r: CGFloat((argb >> 16) & 0xff) / 255.0, g: CGFloat((argb >> 8) & 0xff) / 255.0, b: CGFloat(argb & 0xff) / 255.0, a: CGFloat((argb >> 24) & 0xff) / 255.0) - } - - var argb: UInt32 { - return (UInt32(self.a * 255.0) << 24) | (UInt32(max(0.0, self.r) * 255.0) << 16) | (UInt32(max(0.0, self.g) * 255.0) << 8) | (UInt32(max(0.0, self.b) * 255.0)) - } -} - -private struct NodeFlags: OptionSet { - var rawValue: UInt8 - - init(rawValue: UInt8) { - self.rawValue = rawValue - } - - static let masksToBounds = NodeFlags(rawValue: 1 << 0) - static let isHidden = NodeFlags(rawValue: 1 << 1) - static let hasSimpleContents = NodeFlags(rawValue: 1 << 2) - static let isInvertedMatte = NodeFlags(rawValue: 1 << 3) - - static let hasRenderContent = NodeFlags(rawValue: 1 << 4) - static let hasSubnodes = NodeFlags(rawValue: 1 << 5) - static let hasMask = NodeFlags(rawValue: 1 << 6) -} - -private struct LottieContentFlags: OptionSet { - var rawValue: UInt8 - - init(rawValue: UInt8) { - self.rawValue = rawValue - } - - static let hasStroke = LottieContentFlags(rawValue: 1 << 0) - static let hasFill = LottieContentFlags(rawValue: 1 << 1) -} - -func serializePath(buffer: WriteBuffer, path: LottiePath) { - let lengthOffset = buffer.length - buffer.write(uInt32: 0) - - path.enumerateItems { pathItem in - switch pathItem.pointee.type { - case .moveTo: - let point = pathItem.pointee.points.0 - buffer.write(uInt8: 0) - buffer.write(point: point) - case .lineTo: - let point = pathItem.pointee.points.0 - buffer.write(uInt8: 1) - buffer.write(point: point) - case .curveTo: - let cp1 = pathItem.pointee.points.0 - let cp2 = pathItem.pointee.points.1 - let point = pathItem.pointee.points.2 - - buffer.write(uInt8: 2) - buffer.write(point: cp1) - buffer.write(point: cp2) - buffer.write(point: point) - case .close: - buffer.write(uInt8: 3) - @unknown default: - break - } - } - - let dataLength = buffer.length - lengthOffset - 4 - - let previousLength = buffer.length - buffer.length = lengthOffset - buffer.write(uInt32: UInt32(dataLength)) - buffer.length = previousLength -} - -func deserializePath(buffer: ReadBuffer) -> LottiePath { - let itemDataLength = Int(buffer.readUInt32()) - var itemData = Data(count: itemDataLength) - itemData.withUnsafeMutableBytes { bytes in - buffer.read(bytes: bytes) - } - - return LottiePath(customData: itemData) -} - -func serializeContentShading(buffer: WriteBuffer, shading: LottieRenderContentShading) { - if let shading = shading as? LottieRenderContentSolidShading { - buffer.write(uInt8: 0) - buffer.write(uInt32: shading.color.argb) - buffer.write(uInt8: UInt8(clamping: Int(shading.opacity * 255.0))) - } else if let shading = shading as? LottieRenderContentGradientShading { - buffer.write(uInt8: 1) - buffer.write(uInt8: UInt8(clamping: Int(shading.opacity * 255.0))) - buffer.write(uInt8: UInt8(shading.gradientType.rawValue)) - let colorStopCount = min(shading.colorStops.count, 255) - buffer.write(uInt8: UInt8(colorStopCount)) - for i in 0 ..< colorStopCount { - buffer.write(uInt32: shading.colorStops[i].color.argb) - buffer.write(float: Float(shading.colorStops[i].location)) - } - buffer.write(point: shading.start) - buffer.write(point: shading.end) - } else { - buffer.write(uInt8: 0) - buffer.write(uInt8: UInt8(clamping: Int(1.0 * 255.0))) - } -} - -func deserializeContentShading(buffer: ReadBuffer) -> LottieRenderContentShading { - switch buffer.readUInt8() { - case 0: - return LottieRenderContentSolidShading( - color: LottieColor(argb: buffer.readUInt32()), - opacity: CGFloat(buffer.readUInt8()) / 255.0 - ) - case 1: - let opacity = CGFloat(buffer.readUInt8()) / 255.0 - let gradientType = LottieGradientType(rawValue: UInt(buffer.readUInt8()))! - - var colorStops: [LottieColorStop] = [] - let colorStopCount = Int(buffer.readUInt8()) - for _ in 0 ..< colorStopCount { - colorStops.append(LottieColorStop( - color: LottieColor(argb: buffer.readUInt32()), - location: CGFloat(buffer.readFloat()) - )) - } - - let start = buffer.readPoint() - let end = buffer.readPoint() - - return LottieRenderContentGradientShading( - opacity: opacity, - gradientType: gradientType, - colorStops: colorStops, - start: start, - end: end - ) - default: - preconditionFailure() - } -} - -func serializeStroke(buffer: WriteBuffer, stroke: LottieRenderContentStroke) { - serializeContentShading(buffer: buffer, shading: stroke.shading) - buffer.write(float: Float(stroke.lineWidth)) - buffer.write(uInt8: UInt8(stroke.lineJoin.rawValue)) - buffer.write(uInt8: UInt8(stroke.lineCap.rawValue)) - buffer.write(float: Float(stroke.miterLimit)) -} - -func deserializeStroke(buffer: ReadBuffer) -> LottieRenderContentStroke { - return LottieRenderContentStroke( - shading: deserializeContentShading(buffer: buffer), - lineWidth: CGFloat(buffer.readFloat()), - lineJoin: CGLineJoin(rawValue: Int32(buffer.readUInt8()))!, - lineCap: CGLineCap(rawValue: Int32(buffer.readUInt8()))!, - miterLimit: CGFloat(buffer.readFloat()), - dashPhase: 0.0, - dashPattern: nil - ) -} - -func serializeFill(buffer: WriteBuffer, fill: LottieRenderContentFill) { - serializeContentShading(buffer: buffer, shading: fill.shading) - buffer.write(uInt8: UInt8(fill.fillRule.rawValue)) -} - -func deserializeFill(buffer: ReadBuffer) -> LottieRenderContentFill { - return LottieRenderContentFill( - shading: deserializeContentShading(buffer: buffer), - fillRule: LottieFillRule(rawValue: UInt(buffer.readUInt8()))! - ) -} - -func serializeRenderContent(buffer: WriteBuffer, renderContent: LottieRenderContent) { - var flags: LottieContentFlags = [] - if renderContent.stroke != nil { - flags.insert(.hasStroke) - } - if renderContent.fill != nil { - flags.insert(.hasFill) - } - buffer.write(uInt8: flags.rawValue) - - serializePath(buffer: buffer, path: renderContent.path) - if let stroke = renderContent.stroke { - serializeStroke(buffer: buffer, stroke: stroke) - } - if let fill = renderContent.fill { - serializeFill(buffer: buffer, fill: fill) - } -} - -func deserializeRenderContent(buffer: ReadBuffer) -> LottieRenderContent { - let flags = LottieContentFlags(rawValue: buffer.readUInt8()) - - let path = deserializePath(buffer: buffer) - - var stroke: LottieRenderContentStroke? - if flags.contains(.hasStroke) { - stroke = deserializeStroke(buffer: buffer) - } - - var fill: LottieRenderContentFill? - if flags.contains(.hasFill) { - fill = deserializeFill(buffer: buffer) - } - - return LottieRenderContent( - path: path, - stroke: stroke, - fill: fill - ) -} - -func serializeNode(buffer: WriteBuffer, node: LottieRenderNode) { - var flags: NodeFlags = [] - if node.masksToBounds { - flags.insert(.masksToBounds) - } - if node.isHidden { - flags.insert(.isHidden) - } - if node.hasSimpleContents { - flags.insert(.hasSimpleContents) - } - if node.isInvertedMatte { - flags.insert(.isInvertedMatte) - } - if node.renderContent != nil { - flags.insert(.hasRenderContent) - } - if !node.subnodes.isEmpty { - flags.insert(.hasSubnodes) - } - if node.mask != nil { - flags.insert(.hasMask) - } - - buffer.write(uInt8: flags.rawValue) - - buffer.write(point: node.position) - buffer.write(rect: node.bounds) - buffer.write(transform: node.transform) - buffer.write(uInt8: UInt8(clamping: Int(node.opacity * 255.0))) - buffer.write(rect: node.globalRect) - buffer.write(transform: node.globalTransform) - - if let renderContent = node.renderContent { - serializeRenderContent(buffer: buffer, renderContent: renderContent) - } - if !node.subnodes.isEmpty { - let count = min(node.subnodes.count, 4095) - buffer.write(uInt16: UInt16(count)) - for i in 0 ..< count { - serializeNode(buffer: buffer, node: node.subnodes[i]) - } - } - if let mask = node.mask { - serializeNode(buffer: buffer, node: mask) - } -} - -func deserializeNode(buffer: ReadBuffer) -> LottieRenderNode { - let flags = NodeFlags(rawValue: buffer.readUInt8()) - - let position = buffer.readPoint() - let bounds = buffer.readRect() - let transform = buffer.readTransform() - let opacity = CGFloat(buffer.readUInt8()) / 255.0 - let globalRect = buffer.readRect() - let globalTransform = buffer.readTransform() - - var renderContent: LottieRenderContent? - if flags.contains(.hasRenderContent) { - renderContent = deserializeRenderContent(buffer: buffer) - } - var subnodes: [LottieRenderNode] = [] - if flags.contains(.hasSubnodes) { - let count = Int(buffer.readUInt16()) - for _ in 0 ..< count { - subnodes.append(deserializeNode(buffer: buffer)) - } - } - var mask: LottieRenderNode? - if flags.contains(.hasMask) { - mask = deserializeNode(buffer: buffer) - } - - return LottieRenderNode( - position: position, - bounds: bounds, - transform: transform, - opacity: opacity, - masksToBounds: flags.contains(.masksToBounds), - isHidden: flags.contains(.isHidden), - globalRect: globalRect, - globalTransform: globalTransform, - renderContent: renderContent, - hasSimpleContents: flags.contains(.hasSimpleContents), - isInvertedMatte: flags.contains(.isInvertedMatte), - subnodes: subnodes, - mask: mask - ) -} - -public struct SerializedLottieMetalFrameMapping { - var size: CGSize = CGSize() - var frameCount: Int = 0 - var framesPerSecond: Int = 0 - var frameRanges: [Int: Range] = [:] -} - -func serializeFrameMapping(buffer: WriteBuffer, frameMapping: SerializedLottieMetalFrameMapping) { - buffer.write(size: frameMapping.size) - buffer.write(uInt32: UInt32(frameMapping.frameCount)) - buffer.write(uInt32: UInt32(frameMapping.framesPerSecond)) - for (frame, range) in frameMapping.frameRanges.sorted(by: { $0.key < $1.key }) { - buffer.write(uInt32: UInt32(frame)) - buffer.write(uInt32: UInt32(range.lowerBound)) - buffer.write(uInt32: UInt32(range.upperBound)) - } -} - -func deserializeFrameMapping(buffer: ReadBuffer) -> SerializedLottieMetalFrameMapping { - var frameMapping = SerializedLottieMetalFrameMapping() - - frameMapping.size = buffer.readSize() - frameMapping.frameCount = Int(buffer.readUInt32()) - frameMapping.framesPerSecond = Int(buffer.readUInt32()) - for _ in 0 ..< frameMapping.frameCount { - let frame = Int(buffer.readUInt32()) - let lowerBound = Int(buffer.readUInt32()) - let upperBound = Int(buffer.readUInt32()) - frameMapping.frameRanges[frame] = lowerBound ..< upperBound - } - - return frameMapping -} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift index aeef94f3fa0..ad6000d2ccd 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift @@ -23,6 +23,7 @@ public enum CodableDrawingEntity: Equatable { case bubble(DrawingBubbleEntity) case vector(DrawingVectorEntity) case location(DrawingLocationEntity) + case link(DrawingLinkEntity) public init?(entity: DrawingEntity) { if let entity = entity as? DrawingStickerEntity { @@ -37,6 +38,8 @@ public enum CodableDrawingEntity: Equatable { self = .vector(entity) } else if let entity = entity as? DrawingLocationEntity { self = .location(entity) + } else if let entity = entity as? DrawingLinkEntity { + self = .link(entity) } else { return nil } @@ -56,6 +59,8 @@ public enum CodableDrawingEntity: Equatable { return entity case let .location(entity): return entity + case let .link(entity): + return entity } } @@ -64,6 +69,7 @@ public enum CodableDrawingEntity: Equatable { var size: CGSize? var rotation: CGFloat? var scale: CGFloat? + var cornerRadius: Double? switch self { case let .location(entity): @@ -71,6 +77,9 @@ public enum CodableDrawingEntity: Equatable { size = entity.renderImage?.size rotation = entity.rotation scale = entity.scale + if let size { + cornerRadius = 10.0 / (size.width * entity.scale) + } case let .sticker(entity): var entityPosition = entity.position var entitySize = entity.baseSize @@ -78,7 +87,7 @@ public enum CodableDrawingEntity: Equatable { let entityScale = entity.scale if case .message = entity.content { - let offset: CGFloat = 16.18 * entityScale //54.0 * entityScale / 3.337 + let offset: CGFloat = 16.18 * entityScale entitySize = CGSize(width: entitySize.width - 38.0, height: entitySize.height - 4.0) entityPosition = CGPoint(x: entityPosition.x + offset * cos(entityRotation), y: entityPosition.y + offset * sin(entityRotation)) } @@ -87,6 +96,19 @@ public enum CodableDrawingEntity: Equatable { size = entitySize rotation = entityRotation scale = entityScale + case let .link(entity): + position = entity.position + rotation = entity.rotation + scale = entity.scale + if let entitySize = entity.renderImage?.size { + if entity.whiteImage != nil { + cornerRadius = 38.0 / (entitySize.width * entity.scale) + size = CGSize(width: entitySize.width - 28.0, height: entitySize.height - 26.0) + } else { + cornerRadius = 10.0 / (entitySize.width * entity.scale) + size = entitySize + } + } default: return nil } @@ -95,12 +117,16 @@ public enum CodableDrawingEntity: Equatable { return nil } + let width = size.width * scale / 1080.0 * 100.0 + let height = size.height * scale / 1920.0 * 100.0 + return MediaArea.Coordinates( x: position.x / 1080.0 * 100.0, y: position.y / 1920.0 * 100.0, - width: size.width * scale / 1080.0 * 100.0, - height: size.height * scale / 1920.0 * 100.0, - rotation: rotation / .pi * 180.0 + width: width, + height: height, + rotation: rotation / .pi * 180.0, + cornerRadius: cornerRadius.flatMap { $0 * 100.0 } ) } @@ -116,6 +142,7 @@ public enum CodableDrawingEntity: Equatable { latitude: entity.location.latitude, longitude: entity.location.longitude, venue: entity.location.venue, + address: entity.location.address, queryId: entity.queryId, resultId: entity.resultId ) @@ -135,10 +162,22 @@ public enum CodableDrawingEntity: Equatable { flags: flags ) } else if case let .message(messageIds, _, _, _, _) = entity.content, let messageId = messageIds.first { - return .channelMessage(coordinates: coordinates, messageId: messageId) + return .channelMessage( + coordinates: coordinates, + messageId: messageId + ) } else { return nil } + case let .link(entity): + var url = entity.url + if !url.hasPrefix("http://") && !url.hasPrefix("https://") { + url = "https://\(url)" + } + return .link( + coordinates: coordinates, + url: url + ) default: return nil } @@ -158,6 +197,7 @@ extension CodableDrawingEntity: Codable { case bubble case vector case location + case link } public init(from decoder: Decoder) throws { @@ -176,6 +216,8 @@ extension CodableDrawingEntity: Codable { self = .vector(try container.decode(DrawingVectorEntity.self, forKey: .entity)) case .location: self = .location(try container.decode(DrawingLocationEntity.self, forKey: .entity)) + case .link: + self = .link(try container.decode(DrawingLinkEntity.self, forKey: .entity)) } } @@ -200,6 +242,9 @@ extension CodableDrawingEntity: Codable { case let .location(payload): try container.encode(EntityType.location, forKey: .type) try container.encode(payload, forKey: .entity) + case let .link(payload): + try container.encode(EntityType.link, forKey: .type) + try container.encode(payload, forKey: .entity) } } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingLinkEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingLinkEntity.swift new file mode 100644 index 00000000000..684aa37810d --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingLinkEntity.swift @@ -0,0 +1,260 @@ +import Foundation +import UIKit +import Display +import AccountContext +import TextFormat +import Postbox +import TelegramCore + +public final class DrawingLinkEntity: DrawingEntity, Codable { + private enum CodingKeys: String, CodingKey { + case uuid + case url + case name + case webpage + case positionBelowText + case largeMedia + case expandedSize + case style + case color + case hasCustomColor + case referenceDrawingSize + case position + case width + case scale + case rotation + case renderImage + case whiteImage + case blackImage + } + + public enum Style: Codable, Equatable { + case white + case black + case transparent + case custom + case blur + } + + public var uuid: UUID + public var isAnimated: Bool { + return false + } + + public var url: String + public var name: String + public var webpage: TelegramMediaWebpage? + public var positionBelowText: Bool + public var largeMedia: Bool? + public var expandedSize: CGSize? + public var style: Style + + public var color: DrawingColor = DrawingColor(color: .white) { + didSet { + if self.color.toUIColor().argb == UIColor.white.argb { + self.style = .white + self.hasCustomColor = false + } else { + self.style = .custom + self.hasCustomColor = true + } + } + } + public var hasCustomColor = false + public var lineWidth: CGFloat = 0.0 + + public var referenceDrawingSize: CGSize + public var position: CGPoint + public var width: CGFloat + public var scale: CGFloat { + didSet { + self.scale = min(2.5, self.scale) + } + } + public var rotation: CGFloat + + public var center: CGPoint { + return self.position + } + + public var whiteImage: UIImage? + public var blackImage: UIImage? + + public var renderImage: UIImage? + public var renderSubEntities: [DrawingEntity]? + + public var isMedia: Bool { + return false + } + + public init( + url: String, + name: String, + webpage: TelegramMediaWebpage?, + positionBelowText: Bool, + largeMedia: Bool?, + style: Style + ) { + self.uuid = UUID() + + self.url = url + self.name = name + self.webpage = webpage + self.positionBelowText = positionBelowText + self.largeMedia = largeMedia + self.style = style + + self.referenceDrawingSize = .zero + self.position = .zero + self.width = 100.0 + self.scale = 1.0 + self.rotation = 0.0 + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.uuid = try container.decode(UUID.self, forKey: .uuid) + self.url = try container.decode(String.self, forKey: .url) + self.name = try container.decode(String.self, forKey: .name) + self.positionBelowText = try container.decode(Bool.self, forKey: .positionBelowText) + self.largeMedia = try container.decodeIfPresent(Bool.self, forKey: .largeMedia) + self.style = try container.decode(Style.self, forKey: .style) + + if let webpageData = try container.decodeIfPresent(Data.self, forKey: .webpage) { + self.webpage = PostboxDecoder(buffer: MemoryBuffer(data: webpageData)).decodeRootObject() as? TelegramMediaWebpage + } else { + self.webpage = nil + } + + self.color = try container.decodeIfPresent(DrawingColor.self, forKey: .color) ?? DrawingColor(color: .white) + self.hasCustomColor = try container.decodeIfPresent(Bool.self, forKey: .hasCustomColor) ?? false + + self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) + self.position = try container.decode(CGPoint.self, forKey: .position) + self.width = try container.decode(CGFloat.self, forKey: .width) + self.scale = try container.decode(CGFloat.self, forKey: .scale) + self.rotation = try container.decode(CGFloat.self, forKey: .rotation) + + if let imagePath = try container.decodeIfPresent(String.self, forKey: .whiteImage), let image = UIImage(contentsOfFile: fullEntityMediaPath(imagePath)) { + self.whiteImage = image + } + + if let imagePath = try container.decodeIfPresent(String.self, forKey: .blackImage), let image = UIImage(contentsOfFile: fullEntityMediaPath(imagePath)) { + self.blackImage = image + } + + if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) { + self.renderImage = UIImage(data: renderImageData) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.uuid, forKey: .uuid) + try container.encode(self.url, forKey: .url) + try container.encode(self.name, forKey: .name) + try container.encode(self.positionBelowText, forKey: .positionBelowText) + try container.encodeIfPresent(self.largeMedia, forKey: .largeMedia) + + if let webpage = self.webpage { + let encoder = PostboxEncoder() + encoder.encodeRootObject(webpage) + let webpageData = encoder.makeData() + try container.encode(webpageData, forKey: .webpage) + } else { + try container.encodeNil(forKey: .webpage) + } + + try container.encode(self.style, forKey: .style) + try container.encode(self.color, forKey: .color) + try container.encode(self.hasCustomColor, forKey: .hasCustomColor) + + try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) + try container.encode(self.position, forKey: .position) + try container.encode(self.width, forKey: .width) + try container.encode(self.scale, forKey: .scale) + try container.encode(self.rotation, forKey: .rotation) + + if let image = self.whiteImage { + let imagePath = "\(self.uuid)_white.png" + let fullImagePath = fullEntityMediaPath(imagePath) + if let imageData = image.pngData() { + try? FileManager.default.createDirectory(atPath: entitiesPath(), withIntermediateDirectories: true) + try? imageData.write(to: URL(fileURLWithPath: fullImagePath)) + try container.encodeIfPresent(imagePath, forKey: .whiteImage) + } + } + + if let image = self.blackImage { + let imagePath = "\(self.uuid)black.png" + let fullImagePath = fullEntityMediaPath(imagePath) + if let imageData = image.pngData() { + try? FileManager.default.createDirectory(atPath: entitiesPath(), withIntermediateDirectories: true) + try? imageData.write(to: URL(fileURLWithPath: fullImagePath)) + try container.encodeIfPresent(imagePath, forKey: .blackImage) + } + } + + if let renderImage = self.renderImage, let data = renderImage.pngData() { + try container.encode(data, forKey: .renderImage) + } + } + + public func duplicate(copy: Bool) -> DrawingEntity { + let newEntity = DrawingLinkEntity(url: self.url, name: self.name, webpage: self.webpage, positionBelowText: self.positionBelowText, largeMedia: self.largeMedia, style: self.style) + if copy { + newEntity.uuid = self.uuid + } + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.position = self.position + newEntity.width = self.width + newEntity.scale = self.scale + newEntity.rotation = self.rotation + newEntity.whiteImage = self.whiteImage + newEntity.blackImage = self.blackImage + return newEntity + } + + public func isEqual(to other: DrawingEntity) -> Bool { + guard let other = other as? DrawingLinkEntity else { + return false + } + if self.uuid != other.uuid { + return false + } + if self.url != other.url { + return false + } + if self.name != other.name { + return false + } + if self.webpage != other.webpage { + return false + } + if self.positionBelowText != other.positionBelowText { + return false + } + if self.largeMedia != other.largeMedia { + return false + } + if self.style != other.style { + return false + } + if self.referenceDrawingSize != other.referenceDrawingSize { + return false + } + if self.position != other.position { + return false + } + if self.width != other.width { + return false + } + if self.scale != other.scale { + return false + } + if self.rotation != other.rotation { + return false + } + return true + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift index b280a5231f9..fa8979408a5 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift @@ -5,11 +5,11 @@ import AccountContext import Postbox import TelegramCore -private func entitiesPath() -> String { +func entitiesPath() -> String { return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/mediaEntities" } -private func fullEntityMediaPath(_ path: String) -> String { +func fullEntityMediaPath(_ path: String) -> String { return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/mediaEntities/" + path } @@ -32,6 +32,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case sticker case reaction(MessageReaction.Reaction, ReactionStyle) } + case file(FileMediaReference, FileType) case image(UIImage, ImageType) case animatedImage(Data, UIImage) @@ -135,6 +136,9 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { public var secondaryRenderImage: UIImage? public var overlayRenderImage: UIImage? + public var tertiaryRenderImage: UIImage? + public var quaternaryRenderImage: UIImage? + public var center: CGPoint { return self.position } @@ -418,7 +422,7 @@ public extension UIImage { var images = [UIImage]() var duration = 0.0 - for i in 0.. 0.0 { + finalWidth += leftInset + 6.0 } - - avatarHeaderNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 3.0), size: CGSize(width: layout.size.width, height: avatarHeaderItem.height)) - avatarHeaderNode.updateLayout(size: size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right) - let containerSize = CGSize(width: width + leftInset + 6.0, height: height) - self.frame = CGRect(origin: CGPoint(), size: containerSize) + let containerSize = CGSize(width: finalWidth, height: height) + self.frame = CGRect(origin: CGPoint(x: -1000.0, y: 0.0), size: containerSize) self.messagesContainerNode.frame = CGRect(origin: CGPoint(), size: containerSize) return containerSize @@ -306,13 +329,13 @@ public final class DrawingMessageRenderer { private let nightContainerNode: ContainerNode private let overlayContainerNode: ContainerNode - public init(context: AccountContext, messages: [Message], parentView: UIView) { + public init(context: AccountContext, messages: [Message], parentView: UIView, isLink: Bool = false) { self.context = context self.messages = messages - self.dayContainerNode = ContainerNode(context: context, messages: messages) - self.nightContainerNode = ContainerNode(context: context, messages: messages, isNight: true) - self.overlayContainerNode = ContainerNode(context: context, messages: messages, isOverlay: true) + self.dayContainerNode = ContainerNode(context: context, messages: messages, isLink: isLink) + self.nightContainerNode = ContainerNode(context: context, messages: messages, isNight: true, isLink: isLink) + self.overlayContainerNode = ContainerNode(context: context, messages: messages, isOverlay: true, isLink: isLink) parentView.addSubview(self.dayContainerNode.view) parentView.addSubview(self.nightContainerNode.view) @@ -320,7 +343,7 @@ public final class DrawingMessageRenderer { } public func render(completion: @escaping (Result) -> Void) { - Queue.mainQueue().after(0.1) { + Queue.mainQueue().after(0.12) { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let defaultPresentationData = defaultPresentationData() diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift index 99dc63546ef..b38a45fcad6 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift @@ -28,6 +28,10 @@ private func prerenderTextTransformations(entity: DrawingEntity, image: UIImage, angle = -entity.rotation scale = entity.scale position = entity.position + } else if let entity = entity as? DrawingLinkEntity { + angle = -entity.rotation + scale = entity.scale + position = entity.position } else { fatalError() } @@ -118,6 +122,8 @@ func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, enti return entities } else if let entity = entity as? DrawingLocationEntity { return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)] + } else if let entity = entity as? DrawingLinkEntity { + return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)] } } return [] diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift index 8fadb762122..72690c911d3 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift @@ -59,6 +59,10 @@ public struct MediaEditorResultPrivacy: Codable, Equatable { } public final class MediaEditorDraft: Codable, Equatable { + enum ReadError: Error { + case generic + } + public static func == (lhs: MediaEditorDraft, rhs: MediaEditorDraft) -> Bool { return lhs.path == rhs.path } @@ -117,7 +121,7 @@ public final class MediaEditorDraft: Codable, Equatable { if let thumbnail = UIImage(data: thumbnailData) { self.thumbnail = thumbnail } else { - fatalError() + throw ReadError.generic } self.dimensions = PixelDimensions( width: try container.decode(Int32.self, forKey: .dimensionsWidth), @@ -128,7 +132,7 @@ public final class MediaEditorDraft: Codable, Equatable { if let values = try? JSONDecoder().decode(MediaEditorValues.self, from: valuesData) { self.values = values } else { - fatalError() + throw ReadError.generic } self.caption = ((try? container.decode(ChatTextInputStateText.self, forKey: .caption)) ?? ChatTextInputStateText()).attributedText() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index 9d69ebf7d55..fcbde92d5dd 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -60,6 +60,9 @@ swift_library( "//submodules/UIKitRuntimeUtils", "//submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation", "//submodules/Components/HierarchyTrackingLayer", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/WebsiteType", + "//submodules/UrlEscaping", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift index bc3c5a6c00a..69a75e3123e 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift @@ -88,7 +88,7 @@ final class AdjustmentSliderComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: AdjustmentSliderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: AdjustmentSliderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -104,7 +104,7 @@ final class AdjustmentSliderComponent: Component { } } isTrackingUpdated(isTracking) - let transition: Transition + let transition: ComponentTransition if isTracking { transition = .immediate } else { @@ -219,7 +219,7 @@ final class AdjustmentSliderComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -276,7 +276,7 @@ final class AdjustmentsComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: AdjustmentsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: AdjustmentsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -289,7 +289,7 @@ final class AdjustmentsComponent: Component { let tool = component.tools[i] if tool.key != trackingTool && i < self.toolViews.count { if let view = self.toolViews[i].view { - let transition: Transition + let transition: ComponentTransition if isTracking { transition = .immediate } else { @@ -381,7 +381,7 @@ final class AdjustmentsComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -440,7 +440,7 @@ final class AdjustmentsScreenComponent: Component { } } - func update(component: AdjustmentsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: AdjustmentsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -452,7 +452,7 @@ final class AdjustmentsScreenComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/BlurComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/BlurComponent.swift index cc3918ba747..6164834f2e7 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/BlurComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/BlurComponent.swift @@ -52,7 +52,7 @@ private final class BlurModeComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: BlurModeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BlurModeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -108,7 +108,7 @@ private final class BlurModeComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -186,7 +186,7 @@ final class BlurComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: BlurComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + func update(component: BlurComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state state.value = component.value @@ -302,7 +302,7 @@ final class BlurComponent: Component { component.isTrackingUpdated(isTracking) if let self { - let transition: Transition + let transition: ComponentTransition if isTracking { transition = .immediate } else { @@ -391,7 +391,7 @@ final class BlurComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -751,7 +751,7 @@ final class BlurScreenComponent: Component { return true } - func update(component: BlurScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BlurScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -852,7 +852,7 @@ final class BlurScreenComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkOptions.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkOptions.swift new file mode 100644 index 00000000000..8dbfb6af85d --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkOptions.swift @@ -0,0 +1,235 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import ContextUI +import ChatPresentationInterfaceState +import AccountContext +import TelegramPresentationData +import WebsiteType + +private enum OptionsId: Hashable { + case link +} + +func presentLinkOptionsController(context: AccountContext, selfController: CreateLinkScreen, snapshotImage: UIImage?, isDark: Bool, sourceNode: ASDisplayNode, url: String, name: String, positionBelowText: Bool, largeMedia: Bool?, webPage: TelegramMediaWebpage, completion: @escaping (Bool, Bool?) -> Void, remove: @escaping () -> Void) { + var sources: [ContextController.Source] = [] + + if let source = linkOptions(context: context, selfController: selfController, snapshotImage: snapshotImage, isDark: isDark, sourceNode: sourceNode, url: url, text: name, positionBelowText: positionBelowText, largeMedia: largeMedia, webPage: webPage, completion: completion, remove: remove) { + sources.append(source) + } + if sources.isEmpty { + return + } + + let contextController = ContextController( + presentationData: context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme), + configuration: ContextController.Configuration( + sources: sources, + initialId: AnyHashable(OptionsId.link) + ) + ) + selfController.presentInGlobalOverlay(contextController) +} + +private func linkOptions(context: AccountContext, selfController: CreateLinkScreen, snapshotImage: UIImage?, isDark: Bool, sourceNode: ASDisplayNode, url: String, text: String, positionBelowText: Bool, largeMedia: Bool?, webPage: TelegramMediaWebpage, completion: @escaping (Bool, Bool?) -> Void, remove: @escaping () -> Void) -> ContextController.Source? { + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)) + let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) + + let initialUrlPreview = ChatPresentationInterfaceState.UrlPreview(url: url, webPage: webPage, positionBelowText: positionBelowText, largeMedia: largeMedia) + let urlPreview = ValuePromise(initialUrlPreview) + + let linkOptions = urlPreview.get() + |> deliverOnMainQueue + |> map { urlPreview -> ChatControllerSubject.LinkOptions in + var webpageHasLargeMedia = false + if case let .Loaded(content) = webPage.content { + if let isMediaLargeByDefault = content.isMediaLargeByDefault { + if isMediaLargeByDefault { + webpageHasLargeMedia = true + } + } else { + webpageHasLargeMedia = true + } + } + + + let entities = [MessageTextEntity(range: 0 ..< (text as NSString).length, type: .Url)] + + var largeMedia = false + if webpageHasLargeMedia { + if let value = urlPreview.largeMedia { + largeMedia = value + } else if case .Loaded = webPage.content { + largeMedia = false //!defaultWebpageImageSizeIsSmall(webpage: content) + } else { + largeMedia = true + } + } else { + largeMedia = false + } + + return ChatControllerSubject.LinkOptions( + messageText: text, + messageEntities: entities, + hasAlternativeLinks: false, + replyMessageId: nil, + replyQuote: nil, + url: urlPreview.url, + webpage: urlPreview.webPage, + linkBelowText: urlPreview.positionBelowText, + largeMedia: largeMedia + ) + } + |> distinctUntilChanged + + let wallpaper: TelegramWallpaper? + if let image = snapshotImage { + let wallpaperResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + if let wallpaperData = image.jpegData(compressionQuality: 0.87) { + context.account.postbox.mediaBox.storeResourceData(wallpaperResource.id, data: wallpaperData, synchronous: true) + } + let wallpaperRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(image.size), resource: wallpaperResource, progressiveSizes: [], immediateThumbnailData: nil) + wallpaper = .image([wallpaperRepresentation], WallpaperSettings()) + } else { + wallpaper = nil + } + + let chatController = context.sharedContext.makeChatController( + context: context, + chatLocation: .peer(id: peerId), + subject: .messageOptions(peerIds: [peerId], ids: [], info: .link(ChatControllerSubject.MessageOptionsInfo.Link(options: linkOptions, isCentered: true))), + botStart: nil, + mode: .standard(.previewing), + params: ChatControllerParams( + forcedTheme: isDark ? defaultDarkColorPresentationTheme : defaultPresentationTheme, + forcedNavigationBarTheme: defaultDarkColorPresentationTheme, + forcedWallpaper: wallpaper + ) + ) + chatController.canReadHistory.set(false) + + let items = linkOptions + |> deliverOnMainQueue + |> map { linkOptions -> ContextController.Items in + var items: [ContextMenuItem] = [] + + do { + items.append(.action(ContextMenuActionItem(text: linkOptions.linkBelowText ? presentationData.strings.Conversation_MessageOptionsLinkMoveUp : presentationData.strings.Conversation_MessageOptionsLinkMoveDown, icon: { theme in + return nil + }, iconAnimation: ContextMenuActionItem.IconAnimation( + name: linkOptions.linkBelowText ? "message_preview_sort_above" : "message_preview_sort_below" + ), action: { _, f in + let _ = (urlPreview.get() + |> take(1)).start(next: { current in + var updatedUrlPreview = current + updatedUrlPreview.positionBelowText = !current.positionBelowText + urlPreview.set(updatedUrlPreview) + }) + }))) + } + + if case let .Loaded(content) = linkOptions.webpage.content, let isMediaLargeByDefault = content.isMediaLargeByDefault, isMediaLargeByDefault { + let shrinkTitle: String + let enlargeTitle: String + if let file = content.file, file.isVideo { + shrinkTitle = presentationData.strings.Conversation_MessageOptionsShrinkVideo + enlargeTitle = presentationData.strings.Conversation_MessageOptionsEnlargeVideo + } else { + shrinkTitle = presentationData.strings.Conversation_MessageOptionsShrinkImage + enlargeTitle = presentationData.strings.Conversation_MessageOptionsEnlargeImage + } + + items.append(.action(ContextMenuActionItem(text: linkOptions.largeMedia ? shrinkTitle : enlargeTitle, icon: { _ in + return nil + }, iconAnimation: ContextMenuActionItem.IconAnimation( + name: !linkOptions.largeMedia ? "message_preview_media_large" : "message_preview_media_small" + ), action: { _, f in + let _ = (urlPreview.get() + |> take(1)).start(next: { current in + var updatedUrlPreview = current + if let largeMedia = current.largeMedia { + updatedUrlPreview.largeMedia = !largeMedia + } else { + updatedUrlPreview.largeMedia = false + } + urlPreview.set(updatedUrlPreview) + }) + }))) + } + + if !items.isEmpty { + items.append(.separator) + } + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_MessageOptionsApplyChanges, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + let _ = (urlPreview.get() + |> take(1)).start(next: { current in + completion(current.positionBelowText, current.largeMedia) + }) + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_LinkOptionsCancel, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in + remove() + + f(.default) + }))) + + return ContextController.Items(id: AnyHashable(linkOptions.url), content: .list(items)) + } + + return ContextController.Source( + id: AnyHashable(OptionsId.link), + title: presentationData.strings.Conversation_MessageOptionsTabLink, + source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), + items: items + ) +} + +final class ChatContextControllerContentSourceImpl: ContextControllerContentSource { + let controller: ViewController + weak var sourceNode: ASDisplayNode? + weak var sourceView: UIView? + let sourceRect: CGRect? + + let navigationController: NavigationController? = nil + + let passthroughTouches: Bool + + init(controller: ViewController, sourceNode: ASDisplayNode?, sourceRect: CGRect? = nil, passthroughTouches: Bool) { + self.controller = controller + self.sourceNode = sourceNode + self.sourceRect = sourceRect + self.passthroughTouches = passthroughTouches + } + + init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect? = nil, passthroughTouches: Bool) { + self.controller = controller + self.sourceView = sourceView + self.sourceRect = sourceRect + self.passthroughTouches = passthroughTouches + } + + func transitionInfo() -> ContextControllerTakeControllerInfo? { + let sourceView = self.sourceView + let sourceNode = self.sourceNode + let sourceRect = self.sourceRect + return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in + if let sourceView = sourceView { + return (sourceView, sourceRect ?? sourceView.bounds) + } else if let sourceNode = sourceNode { + return (sourceNode.view, sourceRect ?? sourceNode.bounds) + } else { + return nil + } + }) + } + + func animatedIn() { + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift new file mode 100644 index 00000000000..b8afdb50818 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift @@ -0,0 +1,1034 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import ViewControllerComponent +import SheetComponent +import BalancedTextComponent +import MultilineTextComponent +import BundleIconComponent +import ItemListUI +import AccountContext +import PresentationDataUtils +import ListSectionComponent +import TelegramStringFormatting +import MediaEditor +import UrlEscaping + +private let linkTag = GenericComponentViewTag() +private let nameTag = GenericComponentViewTag() + +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let isEdit: Bool + let link: String + let webpage: TelegramMediaWebpage? + let state: CreateLinkSheetComponent.State + let dismiss: () -> Void + + init( + context: AccountContext, + isEdit: Bool, + link: String, + webpage: TelegramMediaWebpage?, + state: CreateLinkSheetComponent.State, + dismiss: @escaping () -> Void + ) { + self.context = context + self.isEdit = isEdit + self.link = link + self.webpage = webpage + self.state = state + self.dismiss = dismiss + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.isEdit != rhs.isEdit { + return false + } + if lhs.link != rhs.link { + return false + } + if lhs.webpage != rhs.webpage { + return false + } + return true + } + + static var body: Body { + let background = Child(RoundedRectangle.self) + let cancelButton = Child(Button.self) + let doneButton = Child(Button.self) + let title = Child(Text.self) + let urlSection = Child(ListSectionComponent.self) + let nameSection = Child(ListSectionComponent.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let component = context.component + let state = component.state + + let theme = environment.theme.withModalBlocksBackground() + let strings = environment.strings + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let sideInset: CGFloat = 16.0 + var contentSize = CGSize(width: context.availableSize.width, height: 18.0) + + let background = background.update( + component: RoundedRectangle(color: theme.list.blocksBackgroundColor, 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 constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 + + let cancelButton = cancelButton.update( + component: Button( + content: AnyComponent( + Text( + text: strings.Common_Cancel, + font: Font.regular(17.0), + color: theme.actionSheet.controlAccentColor + ) + ), + action: { + component.dismiss() + } + ), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(cancelButton + .position(CGPoint(x: sideInset + cancelButton.size.width / 2.0, y: contentSize.height + cancelButton.size.height / 2.0)) + ) + + let explicitLink = explicitUrl(context.component.link) + var isValidLink = false + if isValidUrl(explicitLink) { + isValidLink = true + } + + let controller = environment.controller + let doneButton = doneButton.update( + component: Button( + content: AnyComponent( + Text( + text: strings.Common_Done, + font: Font.bold(17.0), + color: isValidLink ? theme.actionSheet.controlAccentColor : theme.actionSheet.secondaryTextColor + ) + ), + isEnabled: isValidLink, + action: { [weak state] in + if let controller = controller() as? CreateLinkScreen, let state { + if state.complete(controller: controller) { + component.dismiss() + } + } + } + ), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(doneButton + .position(CGPoint(x: context.availableSize.width - sideInset - doneButton.size.width / 2.0, y: contentSize.height + doneButton.size.height / 2.0)) + ) + + + let title = title.update( + component: Text(text: component.isEdit ? strings.MediaEditor_Link_EditTitle : strings.MediaEditor_Link_CreateTitle, font: Font.bold(17.0), color: theme.list.itemPrimaryTextColor), + 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)) + ) + contentSize.height += title.size.height + contentSize.height += 40.0 + + var urlItems: [AnyComponentWithIdentity] = [] + if let webpage = state.webpage, case .Loaded = webpage.content, !state.dismissed { + urlItems.append( + AnyComponentWithIdentity( + id: "webpage", + component: AnyComponent( + LinkPreviewComponent( + webpage: webpage, + theme: theme, + strings: strings, + presentLinkOptions: { [weak state] sourceNode in + if let controller = controller() as? CreateLinkScreen { + state?.presentLinkOptions(controller: controller, sourceNode: sourceNode) + } + }, + dismiss: { [weak state] in + state?.dismissed = true + state?.updated(transition: .easeInOut(duration: 0.25)) + } + ) + ) + ) + ) + } + urlItems.append( + AnyComponentWithIdentity( + id: "url", + component: AnyComponent( + LinkFieldComponent( + textColor: theme.list.itemPrimaryTextColor, + placeholderColor: theme.list.itemPlaceholderTextColor, + text: state.link, + link: true, + placeholderText: strings.MediaEditor_Link_LinkTo_Placeholder, + textUpdated: { [weak state] text in + state?.link = text + state?.updated() + }, + textReturned: { [weak state] in + if let controller = controller() as? CreateLinkScreen { + state?.switchToNextField(controller: controller) + } + }, + tag: linkTag + ) + ) + ) + ) + + state.selectLink = { + if let controller = controller() as? CreateLinkScreen { + if let view = controller.node.hostView.findTaggedView(tag: linkTag) as? LinkFieldComponent.View { + view.selectAll() + } + } + } + + let urlSection = urlSection.update( + component: ListSectionComponent( + theme: theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.MediaEditor_Link_LinkTo_Title.uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: urlItems, + displaySeparators: false + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(urlSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + urlSection.size.height / 2.0)) + .clipsToBounds(true) + .cornerRadius(10.0) + ) + contentSize.height += urlSection.size.height + contentSize.height += 30.0 + + let nameSection = nameSection.update( + component: ListSectionComponent( + theme: theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.MediaEditor_Link_LinkName_Title.uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: [ + AnyComponentWithIdentity( + id: "name", + component: AnyComponent( + LinkFieldComponent( + textColor: theme.list.itemPrimaryTextColor, + placeholderColor: theme.list.itemPlaceholderTextColor, + text: state.name, + link: false, + placeholderText: strings.MediaEditor_Link_LinkName_Placeholder, + textUpdated: { [weak state] text in + state?.name = text + }, + textReturned: { [weak state] in + if let controller = controller() as? CreateLinkScreen, let state { + if state.complete(controller: controller) { + component.dismiss() + } + } + }, + tag: nameTag + ) + ) + ) + ] + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(nameSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + nameSection.size.height / 2.0)) + .clipsToBounds(true) + .cornerRadius(10.0) + ) + contentSize.height += nameSection.size.height + contentSize.height += 32.0 + + contentSize.height += max(environment.inputHeight, environment.safeInsets.bottom) + + return contentSize + } + } +} + +private final class CreateLinkSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + private let context: AccountContext + private let link: CreateLinkScreen.Link? + + init( + context: AccountContext, + link: CreateLinkScreen.Link? + ) { + self.context = context + self.link = link + } + + static func ==(lhs: CreateLinkSheetComponent, rhs: CreateLinkSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.link != rhs.link { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + + fileprivate var link: String = "" { + didSet { + self.linkPromise.set(self.link) + } + } + fileprivate var name: String = "" + fileprivate var webpage: TelegramMediaWebpage? + fileprivate var isDark = false + fileprivate var dismissed = false + + private var positionBelowText = true + private var largeMedia: Bool? = nil + + private let previewDisposable = MetaDisposable() + + private let linkDisposable = MetaDisposable() + private let linkPromise = ValuePromise() + + var selectLink: () -> Void = {} + + init( + context: AccountContext, + link: CreateLinkScreen.Link? + ) { + self.context = context + + self.link = link?.url ?? "" + self.name = link?.name ?? "" + self.webpage = link?.webpage + self.isDark = link?.isDark ?? false + self.positionBelowText = link?.positionBelowText ?? true + self.largeMedia = link?.largeMedia + + super.init() + + if link == nil { + Queue.mainQueue().after(0.1, { + let pasteboard = UIPasteboard.general + if pasteboard.hasURLs { + if let url = pasteboard.url?.absoluteString, !url.isEmpty { + self.link = url + self.updated() + + self.selectLink() + } + } + }) + } + + self.linkDisposable.set((self.linkPromise.get() + |> delay(1.5, queue: Queue.mainQueue()) + |> deliverOnMainQueue).startStrict(next: { [weak self] link in + guard let self else { + return + } + + guard !link.isEmpty else { + self.dismissed = false + self.previewDisposable.set(nil) + self.webpage = nil + self.updated(transition: .easeInOut(duration: 0.25)) + return + } + + let link = explicitUrl(link) + + if self.dismissed { + self.dismissed = false + self.webpage = nil + } + self.previewDisposable.set( + (webpagePreview(account: context.account, urls: [link]) + |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let self else { + return + } + switch result { + case let .result(result): + self.webpage = result?.webpage + case .progress: + self.webpage = nil + } + self.updated(transition: .easeInOut(duration: 0.25)) + }) + ) + })) + } + + deinit { + self.previewDisposable.dispose() + self.linkDisposable.dispose() + } + + func presentLinkOptions(controller: CreateLinkScreen, sourceNode: ASDisplayNode) { + guard let webpage = self.webpage else { + return + } + let link = explicitUrl(self.link) + var name: String = self.name + if name.isEmpty { + name = self.link + } + + presentLinkOptionsController(context: self.context, selfController: controller, snapshotImage: controller.snapshotImage, isDark: self.isDark, sourceNode: sourceNode, url: link, name: name, positionBelowText: self.positionBelowText, largeMedia: self.largeMedia, webPage: webpage, completion: { [weak self] positionBelowText, largeMedia in + guard let self else { + return + } + self.positionBelowText = positionBelowText + self.largeMedia = largeMedia + }, remove: { [weak self] in + guard let self else { + return + } + self.dismissed = true + self.updated(transition: .easeInOut(duration: 0.25)) + }) + } + + func switchToNextField(controller: CreateLinkScreen) { + if let view = controller.node.hostView.findTaggedView(tag: nameTag) as? LinkFieldComponent.View { + view.activateInput() + } + } + + func complete(controller: CreateLinkScreen) -> Bool { + let explicitLink = explicitUrl(self.link) + if !isValidUrl(explicitLink) { + if let view = controller.node.hostView.findTaggedView(tag: linkTag) as? LinkFieldComponent.View { + view.animateError() + } + return false + } + + let text = !self.name.isEmpty ? self.name : self.link + + var effectiveMedia: TelegramMediaWebpage? + if let webpage = self.webpage, case .Loaded = webpage.content, !self.dismissed { + effectiveMedia = webpage + } + + var attributes: [MessageAttribute] = [] + attributes.append(TextEntitiesMessageAttribute(entities: [.init(range: 0 ..< (text as NSString).length, type: .Url)])) + if !self.dismissed { + attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !self.positionBelowText, forceLargeMedia: self.largeMedia, isManuallyAdded: false, isSafe: true)) + } + + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)) + let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: text, attributes: attributes, media: effectiveMedia.flatMap { [$0] } ?? [], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + + + let completion = controller.completion + let renderer = DrawingMessageRenderer(context: self.context, messages: [message], parentView: controller.view, isLink: true) + renderer.render(completion: { result in + completion( + CreateLinkScreen.Result( + url: self.link, + name: self.name, + webpage: effectiveMedia, + positionBelowText: self.positionBelowText, + largeMedia: self.largeMedia, + image: effectiveMedia != nil ? result.dayImage : nil, + nightImage: effectiveMedia != nil ? result.nightImage : nil + ) + ) + }) + return true + } + } + + func makeState() -> State { + return State(context: self.context, link: self.link) + } + + static var body: Body { + let sheet = Child(SheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + var webpage = context.state.webpage + if context.state.dismissed { + webpage = nil + } + + let link = context.state.link + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + isEdit: context.component.link != nil, + link: link, + webpage: webpage, + state: context.state, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + backgroundColor: .blur(.dark), + followContentSizeChanges: true, + clipsContent: true, + isScrollEnabled: false, + 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)) + ) + + return context.availableSize + } + } +} + +public final class CreateLinkScreen: ViewControllerComponentContainer { + public struct Link: Equatable { + let url: String + let name: String? + let webpage: TelegramMediaWebpage? + let positionBelowText: Bool + let largeMedia: Bool? + let isDark: Bool + + init( + url: String, + name: String?, + webpage: TelegramMediaWebpage?, + positionBelowText: Bool, + largeMedia: Bool?, + isDark: Bool + ) { + self.url = url + self.name = name + self.webpage = webpage + self.positionBelowText = positionBelowText + self.largeMedia = largeMedia + self.isDark = isDark + } + } + + public struct Result { + let url: String + let name: String + let webpage: TelegramMediaWebpage? + let positionBelowText: Bool + let largeMedia: Bool? + let image: UIImage? + let nightImage: UIImage? + } + + private let context: AccountContext + fileprivate let snapshotImage: UIImage? + fileprivate let completion: (CreateLinkScreen.Result) -> Void + + public init( + context: AccountContext, + link: CreateLinkScreen.Link?, + snapshotImage: UIImage?, + completion: @escaping (CreateLinkScreen.Result) -> Void + ) { + self.context = context + self.snapshotImage = snapshotImage + self.completion = completion + + super.init( + context: context, + component: CreateLinkSheetComponent( + context: context, + link: link + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .dark + ) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let view = self.node.hostView.findTaggedView(tag: linkTag) as? LinkFieldComponent.View { + view.activateInput() + } + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} + +private final class LinkFieldComponent: Component { + typealias EnvironmentType = Empty + + let textColor: UIColor + let placeholderColor: UIColor + let text: String + let link: Bool + let placeholderText: String + let textUpdated: (String) -> Void + let textReturned: () -> Void + let tag: AnyObject? + + init( + textColor: UIColor, + placeholderColor: UIColor, + text: String, + link: Bool, + placeholderText: String, + textUpdated: @escaping (String) -> Void, + textReturned: @escaping () -> Void, + tag: AnyObject? = nil + ) { + self.textColor = textColor + self.placeholderColor = placeholderColor + self.text = text + self.link = link + self.placeholderText = placeholderText + self.textUpdated = textUpdated + self.textReturned = textReturned + self.tag = tag + } + + static func ==(lhs: LinkFieldComponent, rhs: LinkFieldComponent) -> Bool { + if lhs.textColor != rhs.textColor { + return false + } + if lhs.placeholderColor != rhs.placeholderColor { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.placeholderText != rhs.placeholderText { + return false + } + return true + } + + final class View: UIView, UITextFieldDelegate, ComponentTaggedView { + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + private let placeholderView: ComponentView + private let textField: TextFieldNodeView + + private var component: LinkFieldComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.placeholderView = ComponentView() + self.textField = TextFieldNodeView(frame: .zero) + + super.init(frame: frame) + + self.textField.delegate = self + self.textField.addTarget(self, action: #selector(self.textChanged(_:)), for: .editingChanged) + + self.addSubview(self.textField) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func textChanged(_ sender: Any) { + let text = self.textField.text ?? "" + self.component?.textUpdated(text) + self.placeholderView.view?.isHidden = !text.isEmpty + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if string == "\n" { + self.component?.textReturned() + return false + } + + let newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) + if let component = self.component, !component.link && newText.count > 48 { + self.animateError() + return false + } + return true + } + + func activateInput() { + self.textField.becomeFirstResponder() + } + + func selectAll() { + self.textField.selectAll(nil) + } + + func animateError() { + self.textField.layer.addShakeAnimation() + let hapticFeedback = HapticFeedback() + hapticFeedback.error() + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: { + let _ = hapticFeedback + }) + } + + func update(component: LinkFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.textField.textColor = component.textColor + self.textField.text = component.text + self.textField.font = Font.regular(17.0) + self.textField.keyboardAppearance = .dark + + if component.link { + self.textField.keyboardType = .default + self.textField.returnKeyType = .next + self.textField.autocorrectionType = .no + self.textField.autocapitalizationType = .none + self.textField.textContentType = .URL + } else { + self.textField.returnKeyType = .done + } + + self.component = component + self.state = state + + let placeholderSize = self.placeholderView.update( + transition: .easeInOut(duration: 0.2), + component: AnyComponent( + Text( + text: component.placeholderText, + font: Font.regular(17.0), + color: component.placeholderColor + ) + ), + environment: {}, + containerSize: availableSize + ) + + let size = CGSize(width: availableSize.width, height: 44.0) + if let placeholderComponentView = self.placeholderView.view { + if placeholderComponentView.superview == nil { + self.insertSubview(placeholderComponentView, at: 0) + } + + placeholderComponentView.frame = CGRect(origin: CGPoint(x: 15.0, y: floorToScreenPixels((size.height - placeholderSize.height) / 2.0) + 1.0 - UIScreenPixel), size: placeholderSize) + + placeholderComponentView.isHidden = !component.text.isEmpty + } + + self.textField.frame = CGRect(x: 15.0, y: 0.0, width: size.width - 30.0, height: 44.0) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class LinkPreviewComponent: Component { + typealias EnvironmentType = Empty + + let webpage: TelegramMediaWebpage + let theme: PresentationTheme + let strings: PresentationStrings + let presentLinkOptions: (ASDisplayNode) -> Void + let dismiss: () -> Void + + init( + webpage: TelegramMediaWebpage, + theme: PresentationTheme, + strings: PresentationStrings, + presentLinkOptions: @escaping (ASDisplayNode) -> Void, + dismiss: @escaping () -> Void + ) { + self.webpage = webpage + self.theme = theme + self.strings = strings + self.presentLinkOptions = presentLinkOptions + self.dismiss = dismiss + } + + static func ==(lhs: LinkPreviewComponent, rhs: LinkPreviewComponent) -> Bool { + if lhs.webpage != rhs.webpage { + return false + } + if lhs.theme !== rhs.theme { + return false + } + return true + } + + final class View: UIView, UITextFieldDelegate { + let closeButton: HighlightableButtonNode + let lineNode: ASImageNode + let iconView: UIImageView + let titleNode: TextNode + private var titleString: NSAttributedString? + + let textNode: TextNode + private var textString: NSAttributedString? + + private var component: LinkPreviewComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.closeButton = HighlightableButtonNode() + + self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) + self.closeButton.displaysAsynchronously = false + + self.lineNode = ASImageNode() + self.lineNode.displayWithoutProcessing = true + self.lineNode.displaysAsynchronously = false + + self.iconView = UIImageView() + self.iconView.image = UIImage(bundleImageName: "Chat/Input/Accessory Panels/LinkSettingsIcon")?.withRenderingMode(.alwaysTemplate) + + self.titleNode = TextNode() + self.titleNode.displaysAsynchronously = false + + self.textNode = TextNode() + self.textNode.displaysAsynchronously = false + + super.init(frame: frame) + + self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) + self.addSubnode(self.closeButton) + + self.addSubnode(self.lineNode) + self.addSubview(self.iconView) + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func closePressed() { + guard let component = self.component else { + return + } + component.dismiss() + } + + private var previousTapTimestamp: Double? + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state, let component = self.component { + let timestamp = CFAbsoluteTimeGetCurrent() + if let previousTapTimestamp = self.previousTapTimestamp, previousTapTimestamp + 1.0 > timestamp { + return + } + self.previousTapTimestamp = CFAbsoluteTimeGetCurrent() + component.presentLinkOptions(self.textNode) + } + } + + func update(component: LinkPreviewComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + self.component = component + self.state = state + + if themeUpdated { + self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(component.theme), for: []) + self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(component.theme) + self.iconView.tintColor = component.theme.chat.inputPanel.panelControlAccentColor + } + + let bounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 45.0)) + + var authorName = "" + var text = "" + switch component.webpage.content { + case .Pending: + authorName = component.strings.Channel_NotificationLoading + text = ""//component.url + case let .Loaded(content): + if let contentText = content.text { + text = contentText + } else { + if let file = content.file, let mediaKind = mediaContentKind(EngineMedia(file)) { + if content.type == "telegram_background" { + text = component.strings.Message_Wallpaper + } else if content.type == "telegram_theme" { + text = component.strings.Message_Theme + } else { + text = stringForMediaKind(mediaKind, strings: component.strings).0.string + } + } else if content.type == "telegram_theme" { + text = component.strings.Message_Theme + } else if content.type == "video" { + text = stringForMediaKind(.video, strings: component.strings).0.string + } else if content.type == "telegram_story" { + text = stringForMediaKind(.story, strings: component.strings).0.string + } else if let _ = content.image { + text = stringForMediaKind(.image, strings: component.strings).0.string + } + } + + if let title = content.title { + authorName = title + } else if let websiteName = content.websiteName { + authorName = websiteName + } else { + authorName = content.displayUrl + } + + } + + self.titleString = NSAttributedString(string: authorName, font: Font.medium(15.0), textColor: component.theme.chat.inputPanel.panelControlAccentColor) + self.textString = NSAttributedString(string: text, font: Font.regular(15.0), textColor: component.theme.chat.inputPanel.primaryTextColor) + + let inset: CGFloat = 0.0 + let leftInset: CGFloat = 55.0 + let textLineInset: CGFloat = 10.0 + let rightInset: CGFloat = 55.0 + let textRightInset: CGFloat = 20.0 + + let closeButtonSize = CGSize(width: 44.0, height: bounds.height) + self.closeButton.frame = CGRect(origin: CGPoint(x: bounds.size.width - closeButtonSize.width - inset, y: 2.0), size: closeButtonSize) + + self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) + + if let icon = self.iconView.image { + self.iconView.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size) + } + + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTextLayout = TextNode.asyncLayout(self.textNode) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: self.titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: self.textString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 7.0), size: titleLayout.size) + + self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 25.0), size: textLayout.size) + + let _ = titleApply() + let _ = textApply() + + return bounds.size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CurvesComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CurvesComponent.swift index 27e38ffde6a..924459dc45b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CurvesComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CurvesComponent.swift @@ -26,7 +26,7 @@ private class HistogramView: UIView { fatalError("init(coder:) has not been implemented") } - func updateSize(size: CGSize, histogramBins: MediaEditorHistogram.HistogramBins?, color: UIColor, transition: Transition) { + func updateSize(size: CGSize, histogramBins: MediaEditorHistogram.HistogramBins?, color: UIColor, transition: ComponentTransition) { guard self.size != size || self.color != color || self.histogramBins != histogramBins else { return } @@ -36,7 +36,7 @@ private class HistogramView: UIView { self.update(transition: transition) } - func update(transition: Transition) { + func update(transition: ComponentTransition) { guard let size = self.size, let histogramBins = self.histogramBins, histogramBins.count > 0, let color = self.color else { self.shapeLayer.path = nil return @@ -141,7 +141,7 @@ final class CurvesComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: CurvesComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + func update(component: CurvesComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -159,7 +159,7 @@ final class CurvesComponent: Component { ), action: { [weak state] in state?.section = .all - state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) + state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut))) } ) ), @@ -187,7 +187,7 @@ final class CurvesComponent: Component { ), action: { [weak state] in state?.section = .red - state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) + state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut))) } ) ), @@ -215,7 +215,7 @@ final class CurvesComponent: Component { ), action: { [weak state] in state?.section = .green - state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) + state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut))) } ) ), @@ -243,7 +243,7 @@ final class CurvesComponent: Component { ), action: { [weak state] in state?.section = .blue - state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) + state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut))) } ) ), @@ -288,7 +288,7 @@ final class CurvesComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -505,7 +505,7 @@ final class CurvesScreenComponent: Component { } } - func update(component: CurvesScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: CurvesScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -730,7 +730,7 @@ final class CurvesScreenComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/FlipButtonContentComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/FlipButtonContentComponent.swift index ae34bafa026..3947ddd1b17 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/FlipButtonContentComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/FlipButtonContentComponent.swift @@ -61,7 +61,7 @@ final class FlipButtonContentComponent: Component { self.icon.add(animation, forKey: "transform.rotation.z") } - func update(component: FlipButtonContentComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: FlipButtonContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component let size = CGSize(width: 48.0, height: 48.0) @@ -82,7 +82,7 @@ final class FlipButtonContentComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift index 10987de836b..c43796108e2 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift @@ -182,7 +182,7 @@ private final class MediaCutoutScreenComponent: Component { overlayAlpha = 0.0 backgroundAlpha = 1.0 } - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) transition.setAlpha(view: overlayView, alpha: overlayAlpha) transition.setAlpha(view: backgroundView, alpha: backgroundAlpha) } @@ -270,7 +270,7 @@ private final class MediaCutoutScreenComponent: Component { return result } - func update(component: MediaCutoutScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + func update(component: MediaCutoutScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value self.environment = environment @@ -458,7 +458,7 @@ private final class MediaCutoutScreenComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -520,7 +520,7 @@ final class MediaCutoutScreen: ViewController { return result } - func requestLayout(transition: Transition) { + func requestLayout(transition: ComponentTransition) { if let layout = self.validLayout { self.containerLayoutUpdated(layout: layout, forceUpdate: true, transition: transition) @@ -530,7 +530,7 @@ final class MediaCutoutScreen: ViewController { } } - func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: ComponentTransition) { guard let controller = self.controller else { return } @@ -692,6 +692,6 @@ final class MediaCutoutScreen: ViewController { override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 1f7551ba8bb..c6758ebf516 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -442,7 +442,7 @@ final class MediaEditorScreenComponent: Component { }, requestLayout: { [weak self] transition in if let self { - (self.environment?.controller() as? MediaEditorScreen)?.node.requestLayout(forceUpdate: true, transition: Transition(transition)) + (self.environment?.controller() as? MediaEditorScreen)?.node.requestLayout(forceUpdate: true, transition: ComponentTransition(transition)) } } ) @@ -556,7 +556,7 @@ final class MediaEditorScreenComponent: Component { func animateOut(to source: TransitionAnimationSource) { self.isDismissed = true - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) if let view = self.cancelButton.view { transition.setAlpha(view: view, alpha: 0.0) transition.setScale(view: view, scale: 0.1) @@ -642,7 +642,7 @@ final class MediaEditorScreenComponent: Component { } } - func animateOutToTool(inPlace: Bool, transition: Transition) { + func animateOutToTool(inPlace: Bool, transition: ComponentTransition) { if let view = self.cancelButton.view { view.alpha = 0.0 } @@ -671,7 +671,7 @@ final class MediaEditorScreenComponent: Component { } } - func animateInFromTool(inPlace: Bool, transition: Transition) { + func animateInFromTool(inPlace: Bool, transition: ComponentTransition) { if let view = self.cancelButton.view { view.alpha = 1.0 } @@ -715,7 +715,7 @@ final class MediaEditorScreenComponent: Component { return inputText } - func update(component: MediaEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + func update(component: MediaEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { guard !self.isDismissed else { return availableSize } @@ -1398,7 +1398,7 @@ final class MediaEditorScreenComponent: Component { } keyboardHeight = inputHeight - let fadeTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) + let fadeTransition = ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)) if self.inputPanelExternalState.isEditing { fadeTransition.setAlpha(view: self.fadeView, alpha: 1.0) } else { @@ -2396,7 +2396,7 @@ final class MediaEditorScreenComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -2543,7 +2543,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var stickerCutoutStatusDisposable: Disposable? fileprivate var isCutout = false - private (set) var hasAnyChanges = false + private(set) var hasAnyChanges = false private var playbackPositionDisposable: Disposable? @@ -3300,6 +3300,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let self { if let location = entity as? DrawingLocationEntity { self.presentLocationPicker(location) + } else if let link = entity as? DrawingLinkEntity { + self.addOrEditLink(link) } } }, @@ -3904,7 +3906,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateOut(to: .camera) } - let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)) transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in completion() }) @@ -3931,7 +3933,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate func animateOutToTool(tool: MediaEditorScreenComponent.DrawingScreenType, inPlace: Bool = false) { self.isDisplayingTool = tool - let transition: Transition = .easeInOut(duration: 0.2) + let transition: ComponentTransition = .easeInOut(duration: 0.2) if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateOutToTool(inPlace: inPlace, transition: transition) } @@ -3941,7 +3943,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate func animateInFromTool(inPlace: Bool = false) { self.isDisplayingTool = nil - let transition: Transition = .easeInOut(duration: 0.2) + let transition: ComponentTransition = .easeInOut(duration: 0.2) if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateInFromTool(inPlace: inPlace, transition: transition) } @@ -4173,73 +4175,74 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } }, completion: { [weak self] location, queryId, resultId, address, countryCode in - if let self { - let emojiFile: Signal - if let countryCode { - let flag = flagEmoji(countryCode: countryCode) - emojiFile = self.staticEmojiPack.get() - |> filter { result in - if case .result = result { - return true - } else { - return false - } - } - |> take(1) - |> map { result -> TelegramMediaFile? in - if case let .result(_, items, _) = result, let match = items.first(where: { item in - var displayText: String? - for attribute in item.file.attributes { - if case let .CustomEmoji(_, _, alt, _) = attribute { - displayText = alt - break - } - } - if let displayText, displayText.hasPrefix(flag) { + if let self { + let emojiFile: Signal + if let countryCode { + let flag = flagEmoji(countryCode: countryCode) + emojiFile = self.staticEmojiPack.get() + |> filter { result in + if case .result = result { return true } else { return false } - }) { - return match.file - } else { - return nil } - } - } else { - emojiFile = .single(nil) - } - - let _ = emojiFile.start(next: { [weak self] emojiFile in - guard let self else { - return - } - let title: String - if let venueTitle = location.venue?.title { - title = venueTitle + |> take(1) + |> map { result -> TelegramMediaFile? in + if case let .result(_, items, _) = result, let match = items.first(where: { item in + var displayText: String? + for attribute in item.file.attributes { + if case let .CustomEmoji(_, _, alt, _) = attribute { + displayText = alt + break + } + } + if let displayText, displayText.hasPrefix(flag) { + return true + } else { + return false + } + }) { + return match.file + } else { + return nil + } + } } else { - title = address ?? "Location" + emojiFile = .single(nil) } - let position = existingEntity?.position - let scale = existingEntity?.scale ?? 1.0 - if let existingEntity { - self.entitiesView.remove(uuid: existingEntity.uuid, animated: true) - } - self.interaction?.insertEntity( - DrawingLocationEntity( - title: title, - style: existingEntity?.style ?? .white, - location: location, - icon: emojiFile, - queryId: queryId, - resultId: resultId - ), - scale: scale, - position: position - ) - }) + + let _ = emojiFile.start(next: { [weak self] emojiFile in + guard let self else { + return + } + let title: String + if let venueTitle = location.venue?.title { + title = venueTitle + } else { + title = address ?? "Location" + } + let position = existingEntity?.position + let scale = existingEntity?.scale ?? 1.0 + if let existingEntity { + self.entitiesView.remove(uuid: existingEntity.uuid, animated: true) + } + self.interaction?.insertEntity( + DrawingLocationEntity( + title: title, + style: existingEntity?.style ?? .white, + location: location, + icon: emojiFile, + queryId: queryId, + resultId: resultId + ), + scale: scale, + position: position + ) + }) + } } - }) + ) locationController.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak locationController] transition in if let self, let locationController { let transitionFactor = locationController.modalStyleOverlayTransitionFactor @@ -4477,6 +4480,69 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.controller?.present(contextController, in: .window(.root)) } + func addOrEditLink(_ existingEntity: DrawingLinkEntity? = nil) { + guard let controller = self.controller else { + return + } + + if existingEntity == nil { + let maxLinkCount = self.context.userLimits.maxStoriesLinksCount + var currentLinkCount = 0 + self.entitiesView.eachView { entityView in + if entityView.entity is DrawingLinkEntity { + currentLinkCount += 1 + } + } + if currentLinkCount >= maxLinkCount { + controller.presentLinkLimitTooltip() + return + } + } + + var link: CreateLinkScreen.Link? + if let existingEntity { + link = CreateLinkScreen.Link( + url: existingEntity.url, + name: existingEntity.name, + webpage: existingEntity.webpage, + positionBelowText: existingEntity.positionBelowText, + largeMedia: existingEntity.largeMedia, + isDark: existingEntity.style == .black + ) + } + + let linkController = CreateLinkScreen(context: controller.context, link: link, snapshotImage: self.mediaEditor?.resultImage, completion: { [weak self] result in + guard let self else { + return + } + + let style: DrawingLinkEntity.Style + if let existingEntity { + if ![.white, .black].contains(existingEntity.style), result.webpage != nil { + style = .white + } else { + style = existingEntity.style + } + } else { + style = .white + } + + let entity = DrawingLinkEntity(url: result.url, name: result.name, webpage: result.webpage, positionBelowText: result.positionBelowText, largeMedia: result.largeMedia, style: style) + entity.whiteImage = result.image + entity.blackImage = result.nightImage + + if let existingEntity { + self.entitiesView.remove(uuid: existingEntity.uuid, animated: true) + } + self.interaction?.insertEntity( + entity, + scale: existingEntity?.scale ?? 1.0, + position: existingEntity?.position + ) + }) + controller.push(linkController) + } + func addReaction() { guard let controller = self.controller else { return @@ -4530,7 +4596,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return result } - func requestUpdate(hasAppeared: Bool = false, transition: Transition = .immediate) { + func requestUpdate(hasAppeared: Bool = false, transition: ComponentTransition = .immediate) { if let layout = self.validLayout { self.containerLayoutUpdated(layout: layout, hasAppeared: hasAppeared, transition: transition) } @@ -4591,14 +4657,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var previousDrawingData: Data? private var previousDrawingEntities: [DrawingEntity]? - func requestLayout(forceUpdate: Bool, transition: Transition) { + func requestLayout(forceUpdate: Bool, transition: ComponentTransition) { guard let layout = self.validLayout else { return } self.containerLayoutUpdated(layout: layout, forceUpdate: forceUpdate, hasAppeared: self.hasAppeared, transition: transition) } - func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, hasAppeared: Bool = false, transition: Transition) { + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, hasAppeared: Bool = false, transition: ComponentTransition) { guard let controller = self.controller, !self.isDismissed else { return } @@ -4750,6 +4816,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate controller?.dismiss(animated: true) } } + controller.addLink = { [weak self, weak controller] in + if let self { + self.addOrEditLink() + + self.stickerScreen = nil + controller?.dismiss(animated: true) + } + } controller.pushController = { [weak self] c in self?.controller?.push(c) } @@ -5676,6 +5750,15 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self?.node.presentGallery() }))) + if self.context.isPremium { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaEditor_Shortcut_Link, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + self?.node.addOrEditLink() + }))) + } items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaEditor_Shortcut_Location, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/LocationSmall"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in @@ -5909,6 +5992,29 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.present(controller, in: .current) } + fileprivate func presentLinkLimitTooltip() { + self.hapticFeedback.impact(.light) + + self.dismissAllTooltips() + + let context = self.context + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let limit: Int32 = 3 + + let value = presentationData.strings.Story_Editor_TooltipLinkLimitValue(limit) + let content: UndoOverlayContent = .info( + title: nil, + text: presentationData.strings.Story_Editor_TooltipReachedLinkLimitText(value).string, + timeout: nil, + customUndoText: nil + ) + + let controller = UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: true, position: .top, animateInAsReplacement: false, action: { _ in + return true + }) + self.present(controller, in: .window(.root)) + } + func maybePresentDiscardAlert() { self.hapticFeedback.impact(.light) if !self.isEligibleForDraft() { @@ -7230,7 +7336,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) } @available(iOSApplicationExtension 11.0, iOS 11.0, *) @@ -7541,7 +7647,7 @@ private final class ToolValueComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ToolValueComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ToolValueComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousValue = self.component?.value self.component = component self.state = state @@ -7608,7 +7714,7 @@ private final class ToolValueComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -7660,7 +7766,7 @@ public final class BlurredGradientComponent: Component { private var gradientBackground = SimpleLayer() private var gradientForeground = SimpleGradientLayer() - public func update(component: BlurredGradientComponent, availableSize: CGSize, transition: Transition) -> CGSize { + public func update(component: BlurredGradientComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component self.isUserInteractionEnabled = false @@ -7706,7 +7812,7 @@ public final class BlurredGradientComponent: Component { return View(color: nil, enableBlur: true) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift index 2336fce88b2..1f94f2ff041 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift @@ -72,7 +72,7 @@ private final class ToolIconComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ToolIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ToolIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -115,7 +115,7 @@ private final class ToolIconComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -333,7 +333,7 @@ private final class MediaToolsScreenComponent: Component { self.state?.updated() } - func update(component: MediaToolsScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + func update(component: MediaToolsScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value self.environment = environment @@ -576,7 +576,7 @@ private final class MediaToolsScreenComponent: Component { var needsHistogram = false let screenSize: CGSize let optionsSize: CGSize - let optionsTransition: Transition = sectionChanged ? .immediate : transition + let optionsTransition: ComponentTransition = sectionChanged ? .immediate : transition switch component.section { case .adjustments: self.curvesState = nil @@ -688,7 +688,7 @@ private final class MediaToolsScreenComponent: Component { }, isTrackingUpdated: { [weak self] isTracking in if let self { - let transition: Transition + let transition: ComponentTransition if isTracking { transition = .immediate } else { @@ -746,7 +746,7 @@ private final class MediaToolsScreenComponent: Component { }, isTrackingUpdated: { [weak self] isTracking in if let self { - let transition: Transition + let transition: ComponentTransition if isTracking { transition = .immediate } else { @@ -798,7 +798,7 @@ private final class MediaToolsScreenComponent: Component { }, isTrackingUpdated: { [weak self] isTracking in if let self { - let transition: Transition + let transition: ComponentTransition if isTracking { transition = .immediate } else { @@ -833,7 +833,7 @@ private final class MediaToolsScreenComponent: Component { }, isTrackingUpdated: { [weak self] isTracking in if let self { - let transition: Transition + let transition: ComponentTransition if isTracking { transition = .immediate } else { @@ -948,7 +948,7 @@ private final class MediaToolsScreenComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -1003,7 +1003,7 @@ public final class MediaToolsScreen: ViewController { } } - func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: ComponentTransition) { guard let controller = self.controller else { return } @@ -1064,7 +1064,7 @@ public final class MediaToolsScreen: ViewController { } } if let layout = self.validLayout { - self.containerLayoutUpdated(layout: layout, transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } } } @@ -1142,6 +1142,6 @@ public final class MediaToolsScreen: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift index 123591de920..2c67a1c6df6 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift @@ -70,7 +70,7 @@ private final class ProgressComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ProgressComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ProgressComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -146,7 +146,7 @@ private final class ProgressComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -182,7 +182,7 @@ private final class BannerComponent: Component { private var component: BannerComponent? private weak var state: EmptyComponentState? - func update(component: BannerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BannerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -235,7 +235,7 @@ private final class BannerComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -308,7 +308,7 @@ public final class SaveProgressScreenComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: SaveProgressScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: SaveProgressScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value self.environment = environment @@ -378,7 +378,7 @@ public final class SaveProgressScreenComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -429,7 +429,7 @@ public final class SaveProgressScreen: ViewController { } } - func containerLayoutUpdated(layout: ContainerViewLayout, transition: Transition) { + func containerLayoutUpdated(layout: ContainerViewLayout, transition: ComponentTransition) { guard let controller = self.controller else { return } @@ -572,6 +572,6 @@ public final class SaveProgressScreen: ViewController { super.containerLayoutUpdated(layout, transition: transition) - (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift index 70d1f620d25..572a04dd49a 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift @@ -46,7 +46,7 @@ private final class AvatarComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -68,7 +68,7 @@ private final class AvatarComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -151,7 +151,7 @@ final class StoryPreviewComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: StoryPreviewComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryPreviewComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -319,7 +319,7 @@ final class StoryPreviewComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift index 77be65864f1..0f6a0afce0f 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift @@ -56,7 +56,7 @@ private final class TintColorComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: TintColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: TintColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -94,7 +94,7 @@ private final class TintColorComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -176,7 +176,7 @@ final class TintComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: TintComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + func update(component: TintComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state state.shadowsValue = component.shadowsValue @@ -314,7 +314,7 @@ final class TintComponent: Component { component.isTrackingUpdated(isTracking) if let self { - let transition: Transition + let transition: ComponentTransition if isTracking { transition = .immediate } else { @@ -403,7 +403,7 @@ final class TintComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift index aa0a996ba51..c08a62d34ec 100644 --- a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift @@ -313,7 +313,7 @@ public final class MediaScrubberComponent: Component { if let offset = self.mainAudioTrackOffset { position += offset } - let transition: Transition = .immediate + let transition: ComponentTransition = .immediate switch gestureRecognizer.state { case .began, .changed: self.isPanningCursor = true @@ -379,7 +379,7 @@ public final class MediaScrubberComponent: Component { self.cursorView.frame = cursorFrame(size: scrubberSize, height: self.effectiveCursorHeight, position: updatedPosition, duration: self.trimDuration) } - public func update(component: MediaScrubberComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(component: MediaScrubberComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let isFirstTime = self.component == nil self.component = component self.state = state @@ -392,7 +392,7 @@ public final class MediaScrubberComponent: Component { } var totalHeight: CGFloat = 0.0 - var trackLayout: [Int32: (CGRect, Transition, Bool)] = [:] + var trackLayout: [Int32: (CGRect, ComponentTransition, Bool)] = [:] if !component.tracks.contains(where: { $0.id == self.selectedTrackId }) { self.selectedTrackId = component.tracks.first(where: { $0.isMain })?.id ?? 0 @@ -636,7 +636,7 @@ public final class MediaScrubberComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -664,7 +664,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega var onSelection: (Int32) -> Void = { _ in } var offsetUpdated: (Double, Bool) -> Void = { _, _ in } - var updated: (Transition) -> Void = { _ in } + var updated: (ComponentTransition) -> Void = { _ in } private(set) var isDragging = false private var ignoreScrollUpdates = false @@ -797,7 +797,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega func updateOpaqueEdges( left: CGFloat, right: CGFloat, - transition: Transition + transition: ComponentTransition ) { self.leftOpaqueEdge = left self.rightOpaqueEdge = right @@ -814,7 +814,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega private func updateThumbnailContainers( scrubberSize: CGSize, availableSize: CGSize, - transition: Transition + transition: ComponentTransition ) { let containerLeftEdge: CGFloat = self.leftOpaqueEdge ?? 0.0 let containerRightEdge: CGFloat = self.rightOpaqueEdge ?? availableSize.width @@ -831,7 +831,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega isSelected: Bool, availableSize: CGSize, duration: Double, - transition: Transition + transition: ComponentTransition ) -> CGSize { let previousParams = self.params self.params = (track, isSelected, availableSize, duration) @@ -1171,7 +1171,7 @@ private class TrimView: UIView { var isHollow = false var trimUpdated: (Double, Double, Bool, Bool) -> Void = { _, _, _, _ in } - var updated: (Transition) -> Void = { _ in } + var updated: (ComponentTransition) -> Void = { _ in } override init(frame: CGRect) { super.init(frame: .zero) @@ -1233,7 +1233,7 @@ private class TrimView: UIView { let startValue = max(0.0, min(params.duration - duration, params.startPosition + delta * params.duration)) let endValue = startValue + duration - var transition: Transition = .immediate + var transition: ComponentTransition = .immediate switch gestureRecognizer.state { case .began, .changed: self.isPanningTrimHandle = true @@ -1273,7 +1273,7 @@ private class TrimView: UIView { endValue -= delta } - var transition: Transition = .immediate + var transition: ComponentTransition = .immediate switch gestureRecognizer.state { case .began, .changed: self.isPanningTrimHandle = true @@ -1311,7 +1311,7 @@ private class TrimView: UIView { startValue += delta } - var transition: Transition = .immediate + var transition: ComponentTransition = .immediate switch gestureRecognizer.state { case .began, .changed: self.isPanningTrimHandle = true @@ -1350,7 +1350,7 @@ private class TrimView: UIView { position: Double, minDuration: Double, maxDuration: Double, - transition: Transition + transition: ComponentTransition ) -> (leftHandleFrame: CGRect, rightHandleFrame: CGRect) { let isFirstTime = self.params == nil self.params = (scrubberSize, duration, startPosition, endPosition, position, minDuration, maxDuration) diff --git a/submodules/TelegramUI/Components/MessageInputActionButtonComponent/Sources/MessageInputActionButtonComponent.swift b/submodules/TelegramUI/Components/MessageInputActionButtonComponent/Sources/MessageInputActionButtonComponent.swift index 874c3d3d1ee..e521dabde9c 100644 --- a/submodules/TelegramUI/Components/MessageInputActionButtonComponent/Sources/MessageInputActionButtonComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputActionButtonComponent/Sources/MessageInputActionButtonComponent.swift @@ -278,7 +278,7 @@ public final class MessageInputActionButtonComponent: Component { let scale: CGFloat = highlighted ? 0.6 : 1.0 - let transition = Transition(animation: .curve(duration: highlighted ? 0.5 : 0.3, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: highlighted ? 0.5 : 0.3, curve: .spring)) transition.setSublayerTransform(view: self, transform: CATransform3DMakeScale(scale, scale, 1.0)) } @@ -310,7 +310,7 @@ public final class MessageInputActionButtonComponent: Component { component.action(component.mode, .up, false) } - func update(component: MessageInputActionButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: MessageInputActionButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component self.componentState = state @@ -321,7 +321,7 @@ public final class MessageInputActionButtonComponent: Component { var transition = transition if transition.animation.isImmediate, let previousComponent, case .like = previousComponent.mode, case .like = component.mode, previousComponent.mode != component.mode, !isFirstTimeForStory { - transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)) } self.containerNode.isUserInteractionEnabled = component.longPressAction != nil @@ -661,7 +661,7 @@ public final class MessageInputActionButtonComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift index 6d096bf69ee..f1a70122bd9 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift @@ -159,13 +159,13 @@ final class ContextResultPanelComponent: Component { fatalError("init(coder:) has not been implemented") } - func animateIn(transition: Transition) { + func animateIn(transition: ComponentTransition) { let offset = self.scrollView.contentOffset.y * -1.0 + 10.0 - Transition.immediate.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset)) + ComponentTransition.immediate.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset)) transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: 0.0)) } - func animateOut(transition: Transition, completion: @escaping () -> Void) { + func animateOut(transition: ComponentTransition, completion: @escaping () -> Void) { let offset = self.scrollView.contentOffset.y * -1.0 + 10.0 self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset), completion: { _ in @@ -179,7 +179,7 @@ final class ContextResultPanelComponent: Component { } } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let itemLayout = self.itemLayout else { return } @@ -273,7 +273,7 @@ final class ContextResultPanelComponent: Component { self.backgroundView.update(size: backgroundSize, cornerRadius: 11.0, transition: transition.containedViewLayoutTransition) } - func update(component: ContextResultPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ContextResultPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { var transition = transition let previousComponent = self.component self.component = component @@ -352,7 +352,7 @@ final class ContextResultPanelComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MediaPreviewPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MediaPreviewPanelComponent.swift index f31ebd0b052..30281224ca8 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MediaPreviewPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MediaPreviewPanelComponent.swift @@ -188,7 +188,7 @@ public final class MediaPreviewPanelComponent: Component { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - public func animateOut(transition: Transition, completion: @escaping () -> Void) { + public func animateOut(transition: ComponentTransition, completion: @escaping () -> Void) { let vibrancyContainer = self.vibrancyContainer transition.setAlpha(view: vibrancyContainer, alpha: 0.0, completion: { [weak vibrancyContainer] _ in vibrancyContainer?.removeFromSuperview() @@ -230,7 +230,7 @@ public final class MediaPreviewPanelComponent: Component { } } - func update(component: MediaPreviewPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: MediaPreviewPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.component == nil, case let .audio(audio) = component.mediaPreview { self.timerTextValue = textForDuration(seconds: audio.duration) } @@ -353,7 +353,7 @@ public final class MediaPreviewPanelComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MediaRecordingPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MediaRecordingPanelComponent.swift index dd438fe314d..c557d0997fa 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MediaRecordingPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MediaRecordingPanelComponent.swift @@ -185,7 +185,7 @@ public final class MediaRecordingPanelComponent: Component { self.vibrancyCancelContainerView.layer.animatePosition(from: CGPoint(x: self.bounds.width, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } - public func animateOut(transition: Transition, dismissRecording: Bool, completion: @escaping () -> Void) { + public func animateOut(transition: ComponentTransition, dismissRecording: Bool, completion: @escaping () -> Void) { guard let component = self.component else { completion() return @@ -201,7 +201,7 @@ public final class MediaRecordingPanelComponent: Component { if let indicatorView = self.indicator.view as? LottieComponent.View { indicatorView.playOnce(completion: { [weak indicatorView] in if let indicatorView { - let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) transition.setScale(view: indicatorView, scale: 0.001) } @@ -238,7 +238,7 @@ public final class MediaRecordingPanelComponent: Component { component.cancelAction() } - func update(component: MediaRecordingPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: MediaRecordingPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component self.state = state @@ -488,7 +488,7 @@ public final class MediaRecordingPanelComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 16102b62511..3c678b46222 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -702,7 +702,7 @@ public final class MessageInputPanelComponent: Component { return result } - func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousPlaceholder = self.component?.placeholder var insets = UIEdgeInsets(top: 14.0, left: 9.0, bottom: 6.0, right: 41.0) @@ -823,7 +823,7 @@ public final class MessageInputPanelComponent: Component { } } - let placeholderTransition: Transition = (previousPlaceholder != nil && previousPlaceholder != component.placeholder) ? Transition(animation: .curve(duration: 0.3, curve: .spring)) : .immediate + let placeholderTransition: ComponentTransition = (previousPlaceholder != nil && previousPlaceholder != component.placeholder) ? ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) : .immediate let placeholderSize = self.placeholder.update( transition: placeholderTransition, component: AnyComponent(AnimatedTextComponent( @@ -1292,7 +1292,7 @@ public final class MessageInputPanelComponent: Component { guard let self, let deleteMediaPreviewButtonView else { return } - let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) transition.setAlpha(view: deleteMediaPreviewButtonView, alpha: 0.0, completion: { [weak deleteMediaPreviewButtonView] _ in deleteMediaPreviewButtonView?.removeFromSuperview() }) @@ -1404,7 +1404,7 @@ public final class MessageInputPanelComponent: Component { self.currentMediaInputIsVoice = !self.currentMediaInputIsVoice self.hapticFeedback.impact(.medium) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) }, updateMediaCancelFraction: { [weak self] mediaCancelFraction in guard let self else { @@ -1787,7 +1787,7 @@ public final class MessageInputPanelComponent: Component { attachmentButtonView.isHidden = true } mediaRecordingPanelView.animateOut(transition: transition, dismissRecording: wasRecordingDismissed, completion: { [weak self, weak mediaRecordingPanelView] in - let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) if let mediaRecordingPanelView = mediaRecordingPanelView { transition.setAlpha(view: mediaRecordingPanelView, alpha: 0.0, completion: { [weak mediaRecordingPanelView] _ in @@ -2166,7 +2166,7 @@ public final class MessageInputPanelComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/StickersResultPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/StickersResultPanelComponent.swift index 18a68035028..ac6dfffa8ec 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/StickersResultPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/StickersResultPanelComponent.swift @@ -335,13 +335,13 @@ final class StickersResultPanelComponent: Component { } } - func animateIn(transition: Transition) { + func animateIn(transition: ComponentTransition) { let offset = self.scrollView.contentOffset.y * -1.0 + 10.0 - Transition.immediate.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset)) + ComponentTransition.immediate.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset)) transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: 0.0)) } - func animateOut(transition: Transition, completion: @escaping () -> Void) { + func animateOut(transition: ComponentTransition, completion: @escaping () -> Void) { let offset = self.scrollView.contentOffset.y * -1.0 + 10.0 self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset), completion: { _ in @@ -355,7 +355,7 @@ final class StickersResultPanelComponent: Component { } } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let itemLayout = self.itemLayout else { return } @@ -422,7 +422,7 @@ final class StickersResultPanelComponent: Component { self.backgroundView.update(size: backgroundSize, cornerRadius: 11.0, transition: transition.containedViewLayoutTransition) } - func update(component: StickersResultPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StickersResultPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { //let itemUpdated = self.component?.results != component.results self.component = component @@ -495,7 +495,7 @@ final class StickersResultPanelComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/TimeoutContentComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/TimeoutContentComponent.swift index c8c854e20ea..f6a139855e3 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/TimeoutContentComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/TimeoutContentComponent.swift @@ -59,7 +59,7 @@ public final class TimeoutContentComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: TimeoutContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: TimeoutContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component self.state = state @@ -120,7 +120,7 @@ public final class TimeoutContentComponent: Component { self.foreground.bounds = CGRect(origin: .zero, size: size) self.foreground.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) - let foregroundTransition: Transition = updated ? .easeInOut(duration: 0.2) : transition + let foregroundTransition: ComponentTransition = updated ? .easeInOut(duration: 0.2) : transition foregroundTransition.setScale(view: self.foreground, scale: component.isSelected ? 1.0 : 0.001) return size @@ -131,7 +131,7 @@ public final class TimeoutContentComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/MinimizedContainer/BUILD b/submodules/TelegramUI/Components/MinimizedContainer/BUILD new file mode 100644 index 00000000000..5f2bbce4969 --- /dev/null +++ b/submodules/TelegramUI/Components/MinimizedContainer/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "MinimizedContainer", + module_name = "MinimizedContainer", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/AccountContext", + "//submodules/UIKitRuntimeUtils", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift new file mode 100644 index 00000000000..7301427ffe4 --- /dev/null +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift @@ -0,0 +1,1004 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import ComponentFlow +import AccountContext +import UIKitRuntimeUtils + +private let minimizedNavigationHeight: CGFloat = 44.0 +private let minimizedTopMargin: CGFloat = 3.0 + +final class ScrollViewImpl: UIScrollView { + var passthrough = false + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if result === self && self.passthrough { + return nil + } + return result + } +} + +public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScrollViewDelegate, ASGestureRecognizerDelegate { + final class Item { + let id: AnyHashable + let controller: ViewController + let beforeMaximize: (NavigationController, @escaping () -> Void) -> Void + + init(id: AnyHashable, controller: ViewController, beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void) { + self.id = id + self.controller = controller + self.beforeMaximize = beforeMaximize + } + } + + final class ItemNode: ASDisplayNode { + var theme: PresentationTheme { + didSet { + if self.theme !== oldValue { + self.headerNode.theme = NavigationControllerTheme(presentationTheme: self.theme) + } + } + } + + var isReady = false + + let item: Item + private let containerNode: ASDisplayNode + private let headerNode: MinimizedHeaderNode + private let dimCoverNode: ASDisplayNode + private let shadowNode: ASImageNode + + private var controllerView: UIView? + fileprivate let snapshotContainerView = UIView() + fileprivate var snapshotView: UIView? + fileprivate var blurredSnapshotView: UIView? + + var tapped: (() -> Void)? + var highlighted: ((Bool) -> Void)? + var closeTapped: (() -> Void)? + + var isCovered: Bool = false { + didSet { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + transition.updateAlpha(node: self.dimCoverNode, alpha: self.isCovered ? 0.25 : 0.0) + } + } + + private var validLayout: (CGSize, UIEdgeInsets, Bool)? + + init(theme: PresentationTheme, strings: PresentationStrings, item: Item) { + self.theme = theme + self.item = item + + self.shadowNode = ASImageNode() + self.shadowNode.clipsToBounds = true + self.shadowNode.cornerRadius = 10.0 + self.shadowNode.displaysAsynchronously = false + self.shadowNode.displayWithoutProcessing = true + self.shadowNode.contentMode = .scaleToFill + self.shadowNode.isUserInteractionEnabled = false + + self.containerNode = ASDisplayNode() + self.containerNode.isUserInteractionEnabled = false + self.containerNode.cornerRadius = 10.0 + + self.headerNode = MinimizedHeaderNode(theme: NavigationControllerTheme(presentationTheme: theme), strings: strings) + self.headerNode.layer.allowsGroupOpacity = true + + self.dimCoverNode = ASDisplayNode() + self.dimCoverNode.alpha = 0.0 + self.dimCoverNode.backgroundColor = UIColor.black + self.dimCoverNode.isUserInteractionEnabled = false + + self.snapshotContainerView.isUserInteractionEnabled = false + + super.init() + + self.clipsToBounds = true + self.cornerRadius = 10.0 + applySmoothRoundedCorners(self.layer) + applySmoothRoundedCorners(self.containerNode.layer) + + self.shadowNode.image = shadowImage + + self.addSubnode(self.containerNode) + self.controllerView = self.item.controller.displayNode.view + self.containerNode.view.addSubview(self.item.controller.displayNode.view) + + Queue.mainQueue().after(0.45) { + self.isReady = true + if !self.isDismissed, let snapshotView = self.controllerView?.snapshotView(afterScreenUpdates: false) { + self.containerNode.view.addSubview(self.snapshotContainerView) + self.snapshotView = snapshotView + self.controllerView?.removeFromSuperview() + self.controllerView = nil + self.snapshotContainerView.addSubview(snapshotView) + self.requestLayout(transition: .immediate) + } + } + + self.addSubnode(self.headerNode) + self.addSubnode(self.dimCoverNode) + self.addSubnode(self.shadowNode) + + self.headerNode.requestClose = { [weak self] in + if let self { + self.closeTapped?() + } + } + + self.headerNode.requestMaximize = { [weak self] in + if let self { + self.tapped?() + } + } + + self.headerNode.controllers = [item.controller] + } + + func setTitleControllers(_ controllers: [ViewController]?) { + self.headerNode.controllers = controllers ?? [self.item.controller] + } + + func animateIn() { + self.headerNode.alpha = 0.0 + let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) + alphaTransition.updateAlpha(node: self.headerNode, alpha: 1.0) + } + + private var isDismissed = false + func animateOut() { + self.isDismissed = true + let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) + transition.updateAlpha(node: self.headerNode, alpha: 0.0) + transition.updateAlpha(node: self.shadowNode, alpha: 0.0) + } + + override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { point in + return .waitForSingleTap + } + recognizer.highlight = { [weak self] point in + if let point = point, point.x > 280.0 { + self?.highlighted?(true) + } else { + self?.highlighted?(false) + } + } + self.view.addGestureRecognizer(recognizer) + } + + @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + guard let (_, insets, _) = self.validLayout, self.isReady else { + return + } + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + if location.x < insets.left + minimizedNavigationHeight && location.y < minimizedNavigationHeight { + self.closeTapped?() + } else { + self.tapped?() + } + default: + break + } + } + default: + break + } + } + + private func requestLayout(transition: ContainedViewLayoutTransition) { + guard let (size, insets, isExpanded) = self.validLayout else { + return + } + self.updateLayout(size: size, insets: insets, isExpanded: isExpanded, transition: transition) + } + + func updateLayout(size: CGSize, insets: UIEdgeInsets, isExpanded: Bool, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, insets, isExpanded) + + var topInset = insets.top + if size.width < size.height { + topInset += 10.0 + } + self.containerNode.frame = CGRect(origin: .zero, size: size) + if let _ = self.item.controller.minimizedTopEdgeOffset { + self.containerNode.subnodeTransform = CATransform3DMakeTranslation(0.0, -topInset, 0.0) + } + + self.snapshotContainerView.frame = CGRect(origin: .zero, size: size) + + self.shadowNode.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: size.height - topInset)) + + var navigationHeight: CGFloat = minimizedNavigationHeight + if !isExpanded { + navigationHeight += insets.bottom + } + + let headerFrame = CGRect(origin: .zero, size: CGSize(width: size.width, height: navigationHeight)) + self.headerNode.update(size: size, insets: insets, isExpanded: isExpanded, transition: transition) + transition.updateFrame(node: self.headerNode, frame: headerFrame) + transition.updateFrame(node: self.dimCoverNode, frame: CGRect(origin: .zero, size: size)) + + if let snapshotView = self.snapshotView { + var snapshotFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - snapshotView.bounds.size.width) / 2.0), y: 0.0), size: snapshotView.bounds.size) + + var requiresBlur = false + var blurFrame = snapshotFrame + if snapshotView.frame.width * 1.1 < size.width { + if let _ = self.item.controller.minimizedTopEdgeOffset { + snapshotFrame = snapshotFrame.offsetBy(dx: 0.0, dy: -66.0) + } + blurFrame = CGRect(origin: CGPoint(x: 0.0, y: snapshotFrame.minY), size: CGSize(width: size.width, height: snapshotFrame.height)) + requiresBlur = true + } else if snapshotView.frame.width > size.width * 1.5 { + if let _ = self.item.controller.minimizedTopEdgeOffset { + snapshotFrame = snapshotFrame.offsetBy(dx: 0.0, dy: 66.0) + } + blurFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - snapshotView.frame.width) / 2.0), y: snapshotFrame.minY), size: CGSize(width: snapshotFrame.width, height: size.height)) + requiresBlur = true + } + + if requiresBlur { + let blurredSnapshotView: UIView? + if let current = self.blurredSnapshotView { + blurredSnapshotView = current + } else { + blurredSnapshotView = snapshotView.snapshotView(afterScreenUpdates: false) + if let blurredSnapshotView { + if let blurFilter = makeBlurFilter() { + blurFilter.setValue(20.0 as NSNumber, forKey: "inputRadius") + blurFilter.setValue(true as NSNumber, forKey: "inputNormalizeEdges") + blurredSnapshotView.layer.filters = [blurFilter] + } + self.snapshotContainerView.insertSubview(blurredSnapshotView, at: 0) + self.blurredSnapshotView = blurredSnapshotView + } + } + blurredSnapshotView?.frame = blurFrame + } else if let blurredSnapshotView = self.blurredSnapshotView { + self.blurredSnapshotView = nil + blurredSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + blurredSnapshotView.removeFromSuperview() + }) + } + transition.updateFrame(view: snapshotView, frame: snapshotFrame) + } + + if !self.isDismissed { + transition.updateAlpha(node: self.shadowNode, alpha: isExpanded ? 1.0 : 0.0) + } + } + } + + private let sharedContext: SharedAccountContext + public weak var navigationController: NavigationController? + private var items: [Item] = [] + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + public private(set) var isExpanded: Bool = false + public var willMaximize: (() -> Void)? + + private let bottomEdgeView: UIImageView + private let blurView: BlurView + private let dimView: UIView + private let scrollView: ScrollViewImpl + private var itemNodes: [AnyHashable: ItemNode] = [:] + + private var highlightedItemId: AnyHashable? + + private var dismissingItemId: AnyHashable? + private var dismissingItemOffset: CGFloat? + + private var expandedTapGestureRecoginzer: UITapGestureRecognizer? + + private var currentTransition: Transition? + private var isApplyingTransition = false + private var validLayout: ContainerViewLayout? + + public var controllers: [ViewController] { + return self.items.map { $0.controller } + } + + public init(sharedContext: SharedAccountContext) { + self.sharedContext = sharedContext + self.presentationData = sharedContext.currentPresentationData.with { $0 } + + self.bottomEdgeView = UIImageView() + self.bottomEdgeView.contentMode = .scaleToFill + self.bottomEdgeView.image = generateImage(CGSize(width: 22.0, height: 24.0), rotatedContext: { size, context in + context.setFillColor(UIColor.black.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + + context.setBlendMode(.clear) + context.setFillColor(UIColor.clear.cgColor) + + let path = UIBezierPath(roundedRect: CGRect(x: 0, y: -10, width: 22, height: 20), cornerRadius: 10) + context.addPath(path.cgPath) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: 11, topCapHeight: 12) + + self.blurView = BlurView(effect: nil) + self.dimView = UIView() + self.dimView.alpha = 0.0 + self.dimView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.6) + self.dimView.isUserInteractionEnabled = false + + self.scrollView = ScrollViewImpl() + self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollView.alwaysBounceVertical = true + + super.init() + + self.view.addSubview(self.bottomEdgeView) + self.view.addSubview(self.blurView) + self.view.addSubview(self.dimView) + self.view.addSubview(self.scrollView) + + self.presentationDataDisposable = (self.sharedContext.presentationData + |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in + guard let self else { + return + } + self.presentationData = presentationData + }) + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + public override func didLoad() { + super.didLoad() + + self.scrollView.delegate = self.wrappedScrollViewDelegate + self.scrollView.alwaysBounceVertical = true + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panGestureRecognizer.delegate = self.wrappedGestureRecognizerDelegate + panGestureRecognizer.delaysTouchesBegan = true + self.scrollView.addGestureRecognizer(panGestureRecognizer) + + let expandedTapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + expandedTapGestureRecoginzer.isEnabled = false + self.expandedTapGestureRecoginzer = expandedTapGestureRecoginzer + self.scrollView.addGestureRecognizer(expandedTapGestureRecoginzer) + } + + func item(at y: CGFloat) -> Int? { + guard let layout = self.validLayout else { + return nil + } + + let insets = layout.insets(options: [.statusBar]) + let itemCount = self.items.count + let spacing = interitemSpacing(itemCount: itemCount, boundingSize: self.scrollView.bounds.size, insets: insets) + return max(0, min(Int(floor((y - additionalInsetTop - insets.top) / spacing)), itemCount - 1)) + } + + public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else { + return false + } + + let location = panGesture.location(in: gestureRecognizer.view) + let velocity = panGesture.velocity(in: gestureRecognizer.view) + + if let _ = self.item(at: location.y) { + if self.isExpanded { + return abs(velocity.x) > abs(velocity.y) + } else { + return abs(velocity.y) > abs(velocity.x) + } + } + return false + } + + @objc func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) { + guard self.isExpanded else { + return + } + if let result = self.scrollView.hitTest(gestureRecognizer.location(in: self.scrollView), with: nil), result === self.scrollView { + self.collapse() + } + } + + @objc func panGesture(_ gestureRecognizer: UIPanGestureRecognizer) { + if self.isExpanded { + self.dismissPanGesture(gestureRecognizer) + } else { + self.expandPanGesture(gestureRecognizer) + } + } + + @objc func expandPanGesture(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let lastItem = self.items.last, let itemNode = self.itemNodes[lastItem.id], itemNode.isReady else { + return + } + let translation = gestureRecognizer.translation(in: self.view) + if translation.y < -10.0 { + gestureRecognizer.isEnabled = false + gestureRecognizer.isEnabled = true + + self.expand() + } + } + + @objc func dismissPanGesture(_ gestureRecognizer: UIPanGestureRecognizer) { + let scrollView = self.scrollView + + switch gestureRecognizer.state { + case .began: + let location = gestureRecognizer.location(in: scrollView) + guard let item = self.item(at: location.y) else { return } + + self.dismissingItemId = self.items[item].id + case .changed: + guard let _ = self.dismissingItemId else { return } + + var delta = gestureRecognizer.translation(in: scrollView) + delta.y = 0 + + if let offset = self.dismissingItemOffset { + self.dismissingItemOffset = offset + delta.x + } else { + self.dismissingItemOffset = delta.x + } + + gestureRecognizer.setTranslation(.zero, in: scrollView) + + self.requestUpdate(transition: .immediate) + case .ended: + var needsLayout = true + if let itemId = self.dismissingItemId { + if let offset = self.dismissingItemOffset { + let velocity = gestureRecognizer.velocity(in: self.view) + if offset < -self.frame.width / 3.0 || velocity.x < -300.0 { + self.currentTransition = .dismiss(itemId: itemId) + + self.items.removeAll(where: { $0.id == itemId }) + if self.items.count == 1 { + self.isExpanded = false + self.willMaximize?() + needsLayout = false + } + } + self.dismissingItemOffset = nil + self.dismissingItemId = nil + } + } + if needsLayout { + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) + } + case .cancelled, .failed: + self.dismissingItemId = nil + default: + break + } + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if result === self.view { + return nil + } + return result + } + + public func addController(_ viewController: ViewController, beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void, transition: ContainedViewLayoutTransition) { + let item = Item( + id: AnyHashable(Int64.random(in: Int64.min ... Int64.max)), + controller: viewController, + beforeMaximize: beforeMaximize + ) + self.items.append(item) + + self.currentTransition = .minimize(itemId: item.id) + self.requestUpdate(transition: transition) + } + + private enum Transition: Equatable { + case minimize(itemId: AnyHashable) + case maximize(itemId: AnyHashable) + case dismiss(itemId: AnyHashable) + case dismissAll + case collapse + + func matches(item: Item) -> Bool { + switch self { + case .minimize: + return false + case let .maximize(itemId), let .dismiss(itemId): + return item.id == itemId + case .dismissAll: + return true + case .collapse: + return false + } + } + } + + public func maximizeController(_ viewController: ViewController, animated: Bool, completion: @escaping (Bool) -> Void) { + guard let item = self.items.first(where: { $0.controller === viewController }) else { + completion(self.items.count == 0) + return + } + if !animated { + self.items.removeAll(where: { $0.id == item.id }) + self.itemNodes[item.id]?.removeFromSupernode() + self.itemNodes[item.id] = nil + completion(self.items.count == 0) + self.scrollView.contentOffset = .zero + return + } + self.isExpanded = false + self.currentTransition = .maximize(itemId: item.id) + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] _ in + guard let self else { + return + } + completion(self.items.count == 0) + self.scrollView.contentOffset = .zero + }) + self.items.removeAll(where: { $0.id == item.id }) + } + + public func dismissAll(completion: @escaping () -> Void) { + self.currentTransition = .dismissAll + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring), completion: { _ in + completion() + }) + } + + public func expand() { + guard !self.items.isEmpty && !self.isExpanded && self.currentTransition == nil else { + return + } + if self.items.count == 1, let item = self.items.first { + if let navigationController = self.navigationController { + item.beforeMaximize(navigationController, { [weak self] in + self?.navigationController?.maximizeViewController(item.controller, animated: true) + }) + } + } else { + self.isExpanded = true + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) + } + } + + public func collapse() { + self.isExpanded = false + self.currentTransition = .collapse + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard self.isExpanded, let layout = self.validLayout else { + return + } + self.requestUpdate(transition: .immediate) + + let contentOffset = scrollView.contentOffset + if scrollView.contentOffset.y < -64.0, let lastItemId = self.items.last?.id, let itemNode = self.itemNodes[lastItemId] { + let velocity = scrollView.panGestureRecognizer.velocity(in: self.view).y + let distance = layout.size.height - self.collapsedHeight(layout: layout) - itemNode.frame.minY + let initialVelocity = distance != 0.0 ? abs(velocity / distance) : 0.0 + + self.isExpanded = false + scrollView.isScrollEnabled = false + scrollView.panGestureRecognizer.isEnabled = false + scrollView.panGestureRecognizer.isEnabled = true + scrollView.contentOffset = contentOffset + self.currentTransition = .collapse + self.requestUpdate(transition: .animated(duration: 0.4, curve: .customSpring(damping: 180.0, initialVelocity: initialVelocity))) + } + } + + private func requestUpdate(transition: ContainedViewLayoutTransition, completion: @escaping (Transition) -> Void = { _ in }) { + guard let layout = self.validLayout else { + return + } + self.updateLayout(layout, transition: transition, completion: completion) + } + + public func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.updateLayout(layout, transition: transition, completion: { _ in }) + } + + private func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, completion: @escaping (Transition) -> Void = { _ in }) { + let isFirstTime = self.validLayout == nil + var containerTransition = transition + if isFirstTime { + containerTransition = .immediate + } + + self.validLayout = layout + + let bounds = CGRect(origin: .zero, size: layout.size) + + containerTransition.updateFrame(view: self.blurView, frame: bounds) + containerTransition.updateFrame(view: self.dimView, frame: bounds) + if self.isExpanded { + if self.blurView.effect == nil { + UIView.animate(withDuration: 0.25, animations: { + self.blurView.effect = UIBlurEffect(style: self.presentationData.theme.overallDarkAppearance ? .dark : .light) + self.dimView.alpha = 1.0 + }) + } + } else { + if self.blurView.effect != nil { + UIView.animate(withDuration: 0.25, animations: { + self.blurView.effect = nil + self.dimView.alpha = 0.0 + }) + } + } + self.blurView.isUserInteractionEnabled = self.isExpanded + + let bottomEdgeHeight = 24.0 + 33.0 + layout.intrinsicInsets.bottom + let bottomEdgeOrigin = layout.size.height - bottomEdgeHeight + containerTransition.updateFrame(view: self.bottomEdgeView, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomEdgeHeight), size: CGSize(width: layout.size.width, height: bottomEdgeHeight))) + + if isFirstTime { + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + transition.animatePosition(layer: self.bottomEdgeView.layer, from: self.bottomEdgeView.layer.position.offsetBy(dx: 0.0, dy: minimizedNavigationHeight + minimizedTopMargin), to: self.bottomEdgeView.layer.position) + } + + if self.isApplyingTransition { + return + } + + let insets = layout.insets(options: [.statusBar]) + let itemInsets = UIEdgeInsets(top: insets.top, left: layout.safeInsets.left, bottom: insets.bottom, right: layout.safeInsets.right) + var topInset = insets.top + if layout.size.width < layout.size.height { + topInset += 10.0 + } + + var index = 0 + let contentHeight = frameForIndex(index: self.items.count - 1, size: layout.size, insets: itemInsets, itemCount: self.items.count, boundingSize: layout.size).midY - 70.0 + for item in self.items { + if let currentTransition = self.currentTransition { + if currentTransition.matches(item: item) { + continue + } else if case .dismiss = currentTransition, self.items.count == 1 { + continue + } + } + + var itemTransition = containerTransition + + let itemNode: ItemNode + if let current = self.itemNodes[item.id] { + itemNode = current + itemNode.theme = self.presentationData.theme + } else { + itemTransition = .immediate + itemNode = ItemNode(theme: self.presentationData.theme, strings: self.presentationData.strings, item: item) + self.scrollView.addSubnode(itemNode) + self.itemNodes[item.id] = itemNode + } + itemNode.closeTapped = { [weak self] in + guard let self else { + return + } + if self.isExpanded { + var needsLayout = true + self.currentTransition = .dismiss(itemId: item.id) + + self.items.removeAll(where: { $0.id == item.id }) + if self.items.count == 1 { + self.isExpanded = false + self.willMaximize?() + needsLayout = false + } + if needsLayout { + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) + } + } else { + self.navigationController?.dismissMinimizedControllers(animated: true) + } + } + itemNode.tapped = { [weak self] in + guard let self else { + return + } + if self.isExpanded { + if let navigationController = self.navigationController { + itemNode.item.beforeMaximize(navigationController, { [weak self] in + self?.navigationController?.maximizeViewController(item.controller, animated: true) + }) + } + } else { + self.expand() + } + } + + let itemFrame: CGRect + let itemTransform: CATransform3D + + if index == self.items.count - 1 { + itemNode.layer.zPosition = 10000.0 + } else { + itemNode.layer.zPosition = 0.0 + } + + if self.isExpanded { + let currentItemFrame = frameForIndex(index: index, size: layout.size, insets: itemInsets, itemCount: self.items.count, boundingSize: layout.size) + let currentItemTransform = final3dTransform(for: currentItemFrame.minY, size: currentItemFrame.size, contentHeight: contentHeight, itemCount: self.items.count, additionalAngle: self.highlightedItemId == item.id ? 0.04 : nil, scrollBounds: self.scrollView.bounds, insets: itemInsets) + + var effectiveItemFrame = currentItemFrame + var effectiveItemTransform = currentItemTransform + + if let dismissingItemId = self.dismissingItemId, let deletingIndex = self.items.firstIndex(where: { $0.id == dismissingItemId }), let offset = self.dismissingItemOffset { + var targetItemFrame: CGRect? + var targetItemTransform: CATransform3D? + if deletingIndex == index { + let effectiveOffset: CGFloat + if offset <= 0.0 { + effectiveOffset = offset + } else { + effectiveOffset = scrollingRubberBandingOffset(offset: offset, bandingStart: 0.0, range: 20.0) + } + effectiveItemFrame = effectiveItemFrame.offsetBy(dx: effectiveOffset, dy: 0.0) + } else if index < deletingIndex { + let frame = frameForIndex(index: index, size: layout.size, insets: itemInsets, itemCount: self.items.count - 1, boundingSize: layout.size) + let spacing = interitemSpacing(itemCount: self.items.count - 1, boundingSize: layout.size, insets: itemInsets) + + targetItemFrame = frame + targetItemTransform = final3dTransform(for: frame.minY, size: layout.size, contentHeight: contentHeight - layout.size.height - spacing, itemCount: self.items.count - 1, scrollBounds: self.scrollView.bounds, insets: itemInsets) + } else { + let frame = frameForIndex(index: index - 1, size: layout.size, insets: itemInsets, itemCount: self.items.count - 1, boundingSize: layout.size) + let spacing = interitemSpacing(itemCount: self.items.count - 1, boundingSize: layout.size, insets: itemInsets) + + targetItemFrame = frame + targetItemTransform = final3dTransform(for: frame.minY, size: layout.size, contentHeight: contentHeight - layout.size.height - spacing, itemCount: self.items.count - 1, scrollBounds: self.scrollView.bounds, insets: itemInsets) + } + + if let targetItemFrame, let targetItemTransform { + let fraction = max(0.0, min(1.0, -1.0 * offset / (layout.size.width * 1.5))) + effectiveItemFrame = effectiveItemFrame.interpolate(with: targetItemFrame, fraction: fraction) + effectiveItemTransform = effectiveItemTransform.interpolate(with: targetItemTransform, fraction: fraction) + } + } + itemFrame = effectiveItemFrame + itemTransform = effectiveItemTransform + + itemNode.isCovered = false + } else { + var itemOffset: CGFloat = bottomEdgeOrigin + 13.0 + var hideTransform = false + if let currentTransition = self.currentTransition { + if case let .maximize(itemId) = currentTransition { + itemOffset += layout.size.height * 0.25 + if let lastItemNode = self.scrollView.subviews.last?.asyncdisplaykit_node as? ItemNode, lastItemNode.item.id == itemId { + hideTransform = true + } + } else if case .dismiss = currentTransition, self.items.count == 1 { + itemOffset += layout.size.height * 0.25 + } + } + + var effectiveItemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemOffset), size: layout.size) + var effectiveItemTransform = itemNode.transform + if hideTransform { + effectiveItemTransform = CATransform3DMakeScale(0.7, 0.7, 1.0) + } else if index == self.items.count - 1 { + if self.items.count > 1 { + effectiveItemFrame = effectiveItemFrame.offsetBy(dx: 0.0, dy: 4.0) + } + effectiveItemTransform = CATransform3DIdentity + } else { + let sideInset: CGFloat = 10.0 + let scaledWidth = layout.size.width - sideInset * 2.0 + let scale = scaledWidth / layout.size.width + let scaledHeight = layout.size.height * scale + let verticalOffset = layout.size.height - scaledHeight + effectiveItemFrame = effectiveItemFrame.offsetBy(dx: 0.0, dy: -verticalOffset / 2.0) + effectiveItemTransform = CATransform3DMakeScale(scale, scale, 1.0) + } + itemFrame = effectiveItemFrame + itemTransform = effectiveItemTransform + + itemNode.isCovered = index <= self.items.count - 2 + } + + itemNode.bounds = CGRect(origin: .zero, size: itemFrame.size) + itemNode.updateLayout(size: itemFrame.size, insets: itemInsets, isExpanded: self.isExpanded, transition: itemTransition) + + if index == self.items.count - 1 && !self.isExpanded { + itemNode.setTitleControllers(self.items.map { $0.controller }) + } else { + itemNode.setTitleControllers(nil) + } + + itemTransition.updateTransform(node: itemNode, transform: itemTransform) + itemTransition.updatePosition(node: itemNode, position: itemFrame.center) + + index += 1 + } + + let contentSize = CGSize(width: layout.size.width, height: contentHeight) + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + if self.scrollView.frame != bounds { + self.scrollView.frame = bounds + } + self.scrollView.passthrough = !self.isExpanded + self.scrollView.isScrollEnabled = self.isExpanded + self.expandedTapGestureRecoginzer?.isEnabled = self.isExpanded + + if let currentTransition = self.currentTransition { + self.isApplyingTransition = true + switch self.currentTransition { + case let .minimize(itemId): + guard let itemNode = self.itemNodes[itemId] else { + return + } + + let dimView = UIView() + dimView.alpha = 1.0 + dimView.frame = CGRect(origin: .zero, size: layout.size) + dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.25) + self.view.insertSubview(dimView, aboveSubview: self.blurView) + dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + dimView.removeFromSuperview() + }) + + itemNode.animateIn() + + var initialOffset = insets.top + if let minimizedTopEdgeOffset = itemNode.item.controller.minimizedTopEdgeOffset { + initialOffset += minimizedTopEdgeOffset + } + if layout.size.width < layout.size.height { + initialOffset += 10.0 + } + if let minimizedBounds = itemNode.item.controller.minimizedBounds { + initialOffset += -minimizedBounds.minY + } + + transition.animatePosition(node: itemNode, from: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + initialOffset), completion: { _ in + self.isApplyingTransition = false + if self.currentTransition == currentTransition { + self.currentTransition = nil + } + completion(currentTransition) + }) + case let .maximize(itemId): + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + guard let itemNode = self.itemNodes[itemId] else { + return + } + + let dimView = UIView() + dimView.frame = CGRect(origin: .zero, size: layout.size) + dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.25) + self.view.insertSubview(dimView, aboveSubview: self.blurView) + dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + + itemNode.animateOut() + if itemInsets.left > 0.0 { + itemNode.updateLayout(size: layout.size, insets: itemInsets, isExpanded: true, transition: transition) + transition.updateBounds(node: itemNode, bounds: CGRect(origin: .zero, size: layout.size)) + } + transition.updateTransform(node: itemNode, transform: CATransform3DIdentity) + transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + topInset + self.scrollView.contentOffset.y), completion: { _ in + self.isApplyingTransition = false + if self.currentTransition == currentTransition { + self.currentTransition = nil + } + + completion(currentTransition) + + if let _ = itemNode.snapshotView { + let snapshotContainerView = itemNode.snapshotContainerView + snapshotContainerView.layer.allowsGroupOpacity = true + snapshotContainerView.center = CGPoint(x: itemNode.item.controller.displayNode.view.bounds.width / 2.0, y: snapshotContainerView.bounds.height / 2.0) + itemNode.item.controller.displayNode.view.addSubview(snapshotContainerView) + Queue.mainQueue().after(0.15, { + snapshotContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + snapshotContainerView.removeFromSuperview() + }) + }) + } + + self.itemNodes[itemId] = nil + itemNode.removeFromSupernode() + dimView.removeFromSuperview() + + self.requestUpdate(transition: .immediate) + }) + case let .dismiss(itemId): + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + guard let dismissedItemNode = self.itemNodes[itemId] else { + return + } + if self.items.count == 1 { + if let itemNode = self.itemNodes.first(where: { $0.0 != itemId })?.value, let navigationController = self.navigationController { + itemNode.item.beforeMaximize(navigationController, { [weak self] in + guard let self else { + return + } + let dimView = UIView() + dimView.frame = CGRect(origin: .zero, size: layout.size) + dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.25) + self.view.insertSubview(dimView, aboveSubview: self.blurView) + dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + + itemNode.animateOut() + transition.updateTransform(node: itemNode, transform: CATransform3DIdentity) + transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + topInset + self.scrollView.contentOffset.y), completion: { _ in + self.isApplyingTransition = false + if self.currentTransition == currentTransition { + self.currentTransition = nil + } + completion(currentTransition) + self.itemNodes[itemId] = nil + itemNode.removeFromSupernode() + dimView.removeFromSuperview() + + self.navigationController?.maximizeViewController(itemNode.item.controller, animated: false) + + self.requestUpdate(transition: .immediate) + }) + }) + } + transition.updatePosition(node: dismissedItemNode, position: CGPoint(x: -layout.size.width, y: dismissedItemNode.position.y)) + } else { + transition.updatePosition(node: dismissedItemNode, position: CGPoint(x: -layout.size.width, y: dismissedItemNode.position.y), completion: { _ in + self.isApplyingTransition = false + if self.currentTransition == currentTransition { + self.currentTransition = nil + } + completion(currentTransition) + + self.itemNodes[itemId] = nil + dismissedItemNode.removeFromSupernode() + }) + } + case .dismissAll: + let dismissOffset = collapsedHeight(layout: layout) + transition.updatePosition(layer: self.bottomEdgeView.layer, position: self.bottomEdgeView.layer.position.offsetBy(dx: 0.0, dy: dismissOffset), completion: { _ in + self.isApplyingTransition = false + if self.currentTransition == currentTransition { + self.currentTransition = nil + } + completion(currentTransition) + }) + transition.updatePosition(layer: self.scrollView.layer, position: self.scrollView.center.offsetBy(dx: 0.0, dy: dismissOffset)) + case .collapse: + transition.updateBounds(layer: self.scrollView.layer, bounds: CGRect(origin: .zero, size: self.scrollView.bounds.size), completion: { _ in + self.isApplyingTransition = false + if self.currentTransition == currentTransition { + self.currentTransition = nil + } + completion(currentTransition) + }) + default: + break + } + } + } + + public func collapsedHeight(layout: ContainerViewLayout) -> CGFloat { + return minimizedNavigationHeight + minimizedTopMargin + layout.intrinsicInsets.bottom + } +} diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift new file mode 100644 index 00000000000..fde07311614 --- /dev/null +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift @@ -0,0 +1,150 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramPresentationData + +final class MinimizedHeaderNode: ASDisplayNode { + var theme: NavigationControllerTheme { + didSet { + self.minimizedBackgroundNode.backgroundColor = self.theme.navigationBar.opaqueBackgroundColor + self.minimizedCloseButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Close"), color: self.theme.navigationBar.primaryTextColor), for: .normal) + } + } + let strings: PresentationStrings + + private let minimizedBackgroundNode: ASDisplayNode + private let minimizedTitleNode: ImmediateTextNode + private let minimizedCloseButton: HighlightableButtonNode + private var minimizedTitleDisposable: Disposable? + + private var _controllers: [Weak] = [] + var controllers: [ViewController] { + get { + return self._controllers.compactMap { $0.value } + } + set { + if !newValue.isEmpty { + if newValue.count != self.controllers.count { + self._controllers = newValue.map { Weak($0) } + + self.minimizedTitleDisposable?.dispose() + self.minimizedTitleDisposable = nil + + var signals: [Signal] = [] + for controller in newValue { + signals.append(controller.titleSignal) + } + + self.minimizedTitleDisposable = (combineLatest(signals) + |> deliverOnMainQueue).start(next: { [weak self] titles in + guard let self else { + return + } + let titles = titles.compactMap { $0 } + if titles.count == 1, let title = titles.first { + self.title = title + } else if let title = titles.last { + let othersString = self.strings.WebApp_MinimizedTitle_Others(Int32(titles.count - 1)) + self.title = self.strings.WebApp_MinimizedTitleFormat(title, othersString).string + } else { + self.title = nil + } + }) + } + } else { + self.minimizedTitleDisposable?.dispose() + self.minimizedTitleDisposable = nil + } + } + } + + var title: String? { + didSet { + if let (size, insets, isExpanded) = self.validLayout { + self.update(size: size, insets: insets, isExpanded: isExpanded, transition: .immediate) + } + } + } + + var requestClose: () -> Void = {} + var requestMaximize: () -> Void = {} + + private var validLayout: (CGSize, UIEdgeInsets, Bool)? + + init(theme: NavigationControllerTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings + + self.minimizedBackgroundNode = ASDisplayNode() + self.minimizedBackgroundNode.cornerRadius = 10.0 + self.minimizedBackgroundNode.clipsToBounds = true + self.minimizedBackgroundNode.backgroundColor = theme.navigationBar.opaqueBackgroundColor + if #available(iOS 11.0, *) { + self.minimizedBackgroundNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } + + self.minimizedTitleNode = ImmediateTextNode() + + self.minimizedCloseButton = HighlightableButtonNode() + self.minimizedCloseButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Close"), color: self.theme.navigationBar.primaryTextColor), for: .normal) + + super.init() + + self.clipsToBounds = true + + self.addSubnode(self.minimizedBackgroundNode) + self.minimizedBackgroundNode.addSubnode(self.minimizedTitleNode) + self.minimizedBackgroundNode.addSubnode(self.minimizedCloseButton) + + self.minimizedCloseButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside) + + applySmoothRoundedCorners(self.minimizedBackgroundNode.layer) + } + + deinit { + self.minimizedTitleDisposable?.dispose() + } + + override func didLoad() { + super.didLoad() + + self.minimizedBackgroundNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.maximizeTapGesture(_:)))) + } + + @objc private func closePressed() { + self.requestClose() + } + + @objc private func maximizeTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let location = recognizer.location(in: self.view) + if location.x < 48.0 { + self.requestClose() + } else { + self.requestMaximize() + } + } + } + + func update(size: CGSize, insets: UIEdgeInsets, isExpanded: Bool, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, insets, isExpanded) + + let headerHeight: CGFloat = 44.0 + var titleSideInset: CGFloat = 56.0 + if !isExpanded { + titleSideInset += insets.left + } + + self.minimizedTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: Font.bold(17.0), textColor: self.theme.navigationBar.primaryTextColor) + + let titleSize = self.minimizedTitleNode.updateLayout(CGSize(width: size.width - titleSideInset * 2.0, height: headerHeight)) + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((headerHeight - titleSize.height) / 2.0)), size: titleSize) + self.minimizedTitleNode.bounds = CGRect(origin: .zero, size: titleFrame.size) + transition.updatePosition(node: self.minimizedTitleNode, position: titleFrame.center) + transition.updateFrame(node: self.minimizedCloseButton, frame: CGRect(origin: CGPoint(x: isExpanded ? 0.0 : insets.left, y: 0.0), size: CGSize(width: 44.0, height: 44.0))) + + transition.updateFrame(node: self.minimizedBackgroundNode, frame: CGRect(origin: .zero, size: CGSize(width: size.width, height: 243.0))) + } +} diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/Utils.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/Utils.swift new file mode 100644 index 00000000000..ee5f726e16d --- /dev/null +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/Utils.swift @@ -0,0 +1,183 @@ +import Foundation +import UIKit +import Display + +extension CATransform3D { + func interpolate(with other: CATransform3D, fraction: CGFloat) -> CATransform3D { + var vectors = Array(repeating: 0.0, count: 16) + vectors[0] = self.m11 + (other.m11 - self.m11) * fraction + vectors[1] = self.m12 + (other.m12 - self.m12) * fraction + vectors[2] = self.m13 + (other.m13 - self.m13) * fraction + vectors[3] = self.m14 + (other.m14 - self.m14) * fraction + vectors[4] = self.m21 + (other.m21 - self.m21) * fraction + vectors[5] = self.m22 + (other.m22 - self.m22) * fraction + vectors[6] = self.m23 + (other.m23 - self.m23) * fraction + vectors[7] = self.m24 + (other.m24 - self.m24) * fraction + vectors[8] = self.m31 + (other.m31 - self.m31) * fraction + vectors[9] = self.m32 + (other.m32 - self.m32) * fraction + vectors[10] = self.m33 + (other.m33 - self.m33) * fraction + vectors[11] = self.m34 + (other.m34 - self.m34) * fraction + vectors[12] = self.m41 + (other.m41 - self.m41) * fraction + vectors[13] = self.m42 + (other.m42 - self.m42) * fraction + vectors[14] = self.m43 + (other.m43 - self.m43) * fraction + vectors[15] = self.m44 + (other.m44 - self.m44) * fraction + + return CATransform3D(m11: vectors[0], m12: vectors[1], m13: vectors[2], m14: vectors[3], m21: vectors[4], m22: vectors[5], m23: vectors[6], m24: vectors[7], m31: vectors[8], m32: vectors[9], m33: vectors[10], m34: vectors[11], m41: vectors[12], m42: vectors[13], m43: vectors[14], m44: vectors[15]) + } +} + +private extension CGFloat { + func interpolate(with other: CGFloat, fraction: CGFloat) -> CGFloat { + let invT = 1.0 - fraction + let result = other * fraction + self * invT + return result + } +} + +private extension CGPoint { + func interpolate(with other: CGPoint, fraction: CGFloat) -> CGPoint { + return CGPoint(x: self.x.interpolate(with: other.x, fraction: fraction), y: self.y.interpolate(with: other.y, fraction: fraction)) + } +} + +private extension CGSize { + func interpolate(with other: CGSize, fraction: CGFloat) -> CGSize { + return CGSize(width: self.width.interpolate(with: other.width, fraction: fraction), height: self.height.interpolate(with: other.height, fraction: fraction)) + } +} + +extension CGRect { + func interpolate(with other: CGRect, fraction: CGFloat) -> CGRect { + return CGRect(origin: self.origin.interpolate(with: other.origin, fraction: fraction), size: self.size.interpolate(with: other.size, fraction: fraction)) + } +} + +private let maxInteritemSpacing: CGFloat = 240.0 +let additionalInsetTop: CGFloat = 16.0 +private let additionalInsetBottom: CGFloat = 0.0 +private let zOffset: CGFloat = -60.0 + +private let perspectiveCorrection: CGFloat = -1.0 / 1000.0 +private let maxRotationAngle: CGFloat = -CGFloat.pi / 2.2 + +func angle(for origin: CGFloat, itemCount: Int, scrollBounds: CGRect, contentHeight: CGFloat?, insets: UIEdgeInsets) -> CGFloat { + var rotationAngle = rotationAngleAt0(itemCount: itemCount) + + var contentOffset = scrollBounds.origin.y + if contentOffset < 0.0 { + contentOffset *= 2.0 + } + + var yOnScreen = origin - contentOffset - additionalInsetTop - insets.top + if yOnScreen < 0 { + yOnScreen = 0 + } else if yOnScreen > scrollBounds.height { + yOnScreen = scrollBounds.height + } + + let maxRotationVariance = maxRotationAngle - rotationAngleAt0(itemCount: itemCount) + rotationAngle += (maxRotationVariance / scrollBounds.height) * yOnScreen + + return rotationAngle +} + +func final3dTransform(for origin: CGFloat, size: CGSize, contentHeight: CGFloat?, itemCount: Int, forcedAngle: CGFloat? = nil, additionalAngle: CGFloat? = nil, scrollBounds: CGRect, insets: UIEdgeInsets) -> CATransform3D { + var transform = CATransform3DIdentity + transform.m34 = perspectiveCorrection + + let rotationAngle = forcedAngle ?? angle(for: origin, itemCount: itemCount, scrollBounds: scrollBounds, contentHeight: contentHeight, insets: insets) + var effectiveRotationAngle = rotationAngle + if let additionalAngle = additionalAngle { + effectiveRotationAngle += additionalAngle + } + + let r = size.height / 2.0 + abs(zOffset / sin(rotationAngle)) + + let zTranslation = r * sin(rotationAngle) + let yTranslation: CGFloat = r * (1 - cos(rotationAngle)) + + let zTranslateTransform = CATransform3DTranslate(transform, 0.0, -yTranslation, zTranslation) + + let rotateTransform = CATransform3DRotate(zTranslateTransform, effectiveRotationAngle, 1.0, 0.0, 0.0) + + return rotateTransform +} + +func interitemSpacing(itemCount: Int, boundingSize: CGSize, insets: UIEdgeInsets) -> CGFloat { + var interitemSpacing = maxInteritemSpacing + if itemCount > 0 { + interitemSpacing = (boundingSize.height - additionalInsetTop - additionalInsetBottom - insets.top) / CGFloat(min(itemCount, 5)) + } + return interitemSpacing +} + +func frameForIndex(index: Int, size: CGSize, insets: UIEdgeInsets, itemCount: Int, boundingSize: CGSize) -> CGRect { + let spacing = interitemSpacing(itemCount: itemCount, boundingSize: boundingSize, insets: insets) + let y = additionalInsetTop + insets.top + spacing * CGFloat(index) + let origin = CGPoint(x: insets.left, y: y) + + return CGRect(origin: origin, size: CGSize(width: size.width - insets.left - insets.right, height: size.height)) +} + +func rotationAngleAt0(itemCount: Int) -> CGFloat { + let multiplier: CGFloat = min(CGFloat(itemCount), 5.0) - 1.0 + return -CGFloat.pi / 7.0 - CGFloat.pi / 7.0 * multiplier / 4.0 +} + +final class BlurView: UIVisualEffectView { + private func setup() { + for subview in self.subviews { + if subview.description.contains("VisualEffectSubview") { + subview.isHidden = true + } + } + + if let sublayer = self.layer.sublayers?[0], let filters = sublayer.filters { + sublayer.backgroundColor = nil + sublayer.isOpaque = false + let allowedKeys: [String] = [ + "gaussianBlur", + "colorSaturate" + ] + sublayer.filters = filters.filter { filter in + guard let filter = filter as? NSObject else { + return true + } + let filterName = String(describing: filter) + if !allowedKeys.contains(filterName) { + return false + } + return true + } + } + } + + override var effect: UIVisualEffect? { + get { + return super.effect + } + set { + super.effect = newValue + self.setup() + } + } + + override func didAddSubview(_ subview: UIView) { + super.didAddSubview(subview) + self.setup() + } +} + +let shadowImage: UIImage? = { + return generateImage(CGSize(width: 1.0, height: 480.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor] as CFArray + + var locations: [CGFloat] = [0.0, 0.65, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: bounds.height), options: []) + }) +}() diff --git a/submodules/TelegramUI/Components/NavigationSearchComponent/Sources/NavigationSearchComponent.swift b/submodules/TelegramUI/Components/NavigationSearchComponent/Sources/NavigationSearchComponent.swift index 6f311df38dd..ded03298167 100644 --- a/submodules/TelegramUI/Components/NavigationSearchComponent/Sources/NavigationSearchComponent.swift +++ b/submodules/TelegramUI/Components/NavigationSearchComponent/Sources/NavigationSearchComponent.swift @@ -167,7 +167,7 @@ public final class NavigationSearchComponent: Component { } } - func update(component: NavigationSearchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: NavigationSearchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component @@ -332,7 +332,7 @@ public final class NavigationSearchComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/OptionButtonComponent/Sources/OptionButtonComponent.swift b/submodules/TelegramUI/Components/OptionButtonComponent/Sources/OptionButtonComponent.swift index d8c3b8e9631..7d6679745ae 100644 --- a/submodules/TelegramUI/Components/OptionButtonComponent/Sources/OptionButtonComponent.swift +++ b/submodules/TelegramUI/Components/OptionButtonComponent/Sources/OptionButtonComponent.swift @@ -64,7 +64,7 @@ public final class OptionButtonComponent: Component { guard let self else { return } - let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)) let scale: CGFloat = highlighed ? 0.8 : 1.0 transition.setSublayerTransform(view: self, transform: CATransform3DMakeScale(scale, scale, 1.0)) } @@ -85,7 +85,7 @@ public final class OptionButtonComponent: Component { component.action() } - func update(component: OptionButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: OptionButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component @@ -135,7 +135,7 @@ public final class OptionButtonComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiListInputComponent.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiListInputComponent.swift index df49f1a2642..44cef176daa 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiListInputComponent.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiListInputComponent.swift @@ -164,7 +164,7 @@ final class EmojiListInputComponent: Component { } } - func update(component: EmojiListInputComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EmojiListInputComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let verticalInset: CGFloat = 12.0 let placeholderSpacing: CGFloat = 6.0 @@ -339,7 +339,7 @@ final class EmojiListInputComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiSelectionComponent.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiSelectionComponent.swift index 0fa2c00af17..eca6988d5e8 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiSelectionComponent.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiSelectionComponent.swift @@ -152,13 +152,13 @@ public final class EmojiSelectionComponent: Component { deinit { } - public func internalRequestUpdate(transition: Transition) { + public func internalRequestUpdate(transition: ComponentTransition) { if let keyboardComponentView = self.keyboardView.view as? EntityKeyboardComponent.View { keyboardComponentView.state?.updated(transition: transition) } } - func update(component: EmojiSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EmojiSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.backgroundColor = component.backgroundColor let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85) self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate) @@ -318,7 +318,7 @@ public final class EmojiSelectionComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/ListSwitchItemComponent.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/ListSwitchItemComponent.swift index 5bc2a486423..8a079687eaa 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/ListSwitchItemComponent.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/ListSwitchItemComponent.swift @@ -52,7 +52,7 @@ final class ListSwitchItemComponent: Component { preconditionFailure() } - func update(component: ListSwitchItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ListSwitchItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -110,7 +110,7 @@ final class ListSwitchItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index d131341e7cf..ba062d1d2af 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -60,6 +60,7 @@ final class PeerAllowedReactionsScreenComponent: Component { private var reactionsInfoText: ComponentView? private var reactionInput: ComponentView? private var reactionCountSection: ComponentView? + private var paidReactionsSection: ComponentView? private let actionButton = ComponentView() private var reactionSelectionControl: ComponentView? @@ -79,6 +80,8 @@ final class PeerAllowedReactionsScreenComponent: Component { private var allowedReactionCount: Int = 11 private var appliedReactionSettings: PeerReactionSettings? + private var areStarsReactionsEnabled: Bool = true + private var emojiContent: EmojiPagerContentComponent? private var emojiContentDisposable: Disposable? private var caretPosition: Int? @@ -93,6 +96,8 @@ final class PeerAllowedReactionsScreenComponent: Component { private weak var currentUndoController: UndoOverlayController? + private var cachedChevronImage: (UIImage, PresentationTheme)? + override init(frame: CGRect) { self.scrollView = UIScrollView() self.scrollView.showsVerticalScrollIndicator = true @@ -185,7 +190,7 @@ final class PeerAllowedReactionsScreenComponent: Component { self.updateScrolling(transition: .immediate) } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { let navigationAlphaDistance: CGFloat = 16.0 let navigationAlpha: CGFloat = max(0.0, min(1.0, self.scrollView.contentOffset.y / navigationAlphaDistance)) if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { @@ -328,7 +333,7 @@ final class PeerAllowedReactionsScreenComponent: Component { self.environment?.controller()?.push(statsController) } - func update(component: PeerAllowedReactionsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PeerAllowedReactionsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -862,7 +867,110 @@ final class PeerAllowedReactionsScreenComponent: Component { } } contentHeight += reactionCountSectionSize.height - contentHeight += 12.0 + + if "".isEmpty { + contentHeight += 32.0 + + let paidReactionsSection: ComponentView + if let current = self.paidReactionsSection { + paidReactionsSection = current + } else { + paidReactionsSection = ComponentView() + self.paidReactionsSection = paidReactionsSection + } + + //TODO:localize + let parsedString = parseMarkdownIntoAttributedString("Switch this on to let your subscribers set paid reactions with Telegram Stars, which you will be able to withdraw later as TON. [Learn More >](https://telegram.org/privacy)", attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { url in + return ("URL", url) + })) + + let paidReactionsFooterText = NSMutableAttributedString(attributedString: parsedString) + + if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { + self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) + } + if let range = paidReactionsFooterText.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { + paidReactionsFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: paidReactionsFooterText.string)) + } + + //TODO:localize + let paidReactionsSectionSize = paidReactionsSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(paidReactionsFooterText), + maximumNumberOfLines: 0, + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + }, tapAction: { [weak self] attributes, _ in + guard let self, let component = self.component else { + return + } + if let url = attributes[NSAttributedString.Key(rawValue: "URL")] as? String { + component.context.sharedContext.applicationBindings.openUrl(url) + } + } + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListSwitchItemComponent( + theme: environment.theme, + title: "Enable Paid Reactions", + value: areStarsReactionsEnabled, + valueUpdated: { [weak self] value in + guard let self else { + return + } + self.areStarsReactionsEnabled = value + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let paidReactionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: paidReactionsSectionSize) + if let paidReactionsSectionView = paidReactionsSection.view { + if paidReactionsSectionView.superview == nil { + self.scrollView.addSubview(paidReactionsSectionView) + } + if animateIn { + paidReactionsSectionView.frame = paidReactionsSectionFrame + if !transition.animation.isImmediate { + paidReactionsSectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } else { + transition.setFrame(view: paidReactionsSectionView, frame: paidReactionsSectionFrame) + } + } + contentHeight += paidReactionsSectionSize.height + contentHeight += 12.0 + } else { + contentHeight += 12.0 + + if let paidReactionsSection = self.paidReactionsSection { + self.paidReactionsSection = nil + if let paidReactionsSectionView = paidReactionsSection.view { + if !transition.animation.isImmediate { + paidReactionsSectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak paidReactionsSectionView] _ in + paidReactionsSectionView?.removeFromSuperview() + }) + } else { + paidReactionsSectionView.removeFromSuperview() + } + } + } + } } else { if let reactionsTitleText = self.reactionsTitleText { self.reactionsTitleText = nil @@ -915,6 +1023,19 @@ final class PeerAllowedReactionsScreenComponent: Component { } } } + + if let paidReactionsSection = self.paidReactionsSection { + self.paidReactionsSection = nil + if let paidReactionsSectionView = paidReactionsSection.view { + if !transition.animation.isImmediate { + paidReactionsSectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak paidReactionsSectionView] _ in + paidReactionsSectionView?.removeFromSuperview() + }) + } else { + paidReactionsSectionView.removeFromSuperview() + } + } + } } var buttonContents: [AnyComponentWithIdentity] = [] @@ -1099,7 +1220,7 @@ final class PeerAllowedReactionsScreenComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift index c3333137e26..2b85d1e5f8c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift @@ -379,7 +379,7 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, AS let threadId = peerData.peer.peerId.toInt64() let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .replyThread(message: ChatReplyThreadMessage( peerId: self.context.account.peerId, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: false, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false - )), subject: nil, botStart: nil, mode: .standard(.previewing)) + )), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) let source: ContextContentSource = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: parentController.navigationController as? NavigationController)) @@ -400,7 +400,7 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, AS public func activateSearch() { if self.chatController == nil { - let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: self.context.account.peerId), subject: nil, botStart: nil, mode: .standard(.embedded(invertDirection: false))) + let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: self.context.account.peerId), subject: nil, botStart: nil, mode: .standard(.embedded(invertDirection: false)), params: nil) chatController.alwaysShowSearchResultsAsList = true chatController.includeSavedPeersInSearchResults = true self.chatController = chatController @@ -440,7 +440,7 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, AS chatController.displayNode.layer.allowsGroupOpacity = true if transition.isAnimated { - Transition.easeInOut(duration: 0.2).setAlpha(layer: chatController.displayNode.layer, alpha: 1.0) + ComponentTransition.easeInOut(duration: 0.2).setAlpha(layer: chatController.displayNode.layer, alpha: 1.0) } if self.searchNavigationContentNode?.contentNode !== contentNode { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift index c8d05f4e09e..1e10096d4d4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift @@ -149,7 +149,7 @@ public final class PeerInfoChatPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScro self.coveringView = UIView() - self.chatController = context.sharedContext.makeChatController(context: context, chatLocation: .replyThread(message: ChatReplyThreadMessage(peerId: context.account.peerId, threadId: peerId.toInt64(), channelMessageId: nil, isChannelPost: false, isForumPost: false, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)), subject: nil, botStart: nil, mode: .standard(.embedded(invertDirection: true))) + self.chatController = context.sharedContext.makeChatController(context: context, chatLocation: .replyThread(message: ChatReplyThreadMessage(peerId: context.account.peerId, threadId: peerId.toInt64(), channelMessageId: nil, isChannelPost: false, isForumPost: false, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)), subject: nil, botStart: nil, mode: .standard(.embedded(invertDirection: true)), params: nil) self.chatController.navigation_setNavigationController(navigationController()) super.init() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift index 855a87fe758..b41f278d4ea 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift @@ -235,7 +235,7 @@ public final class PeerInfoCoverComponent: Component { } } - func update(component: PeerInfoCoverComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PeerInfoCoverComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.component?.peer?.profileBackgroundEmojiId != component.peer?.profileBackgroundEmojiId { if let profileBackgroundEmojiId = component.peer?.profileBackgroundEmojiId, profileBackgroundEmojiId != 0 { if self.patternContentsTarget == nil { @@ -412,7 +412,7 @@ public final class PeerInfoCoverComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index b6ef64e16f8..5f5401456d1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -156,6 +156,7 @@ swift_library( "//submodules/TelegramUI/Components/Settings/PeerSelectionScreen", "//submodules/ConfettiEffect", "//submodules/ContactsPeerItem", + "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift index ce140b8747e..1d61323f753 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift @@ -137,7 +137,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { lineWidth: 3.0, inactiveLineWidth: 1.5, forceRoundedRect: isForum - ), transition: Transition(transition)) + ), transition: ComponentTransition(transition)) } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 46a626b9510..714a60105a1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -337,8 +337,8 @@ final class PeerInfoScreenData { let groupsInCommon: GroupsInCommonContext? let linkedDiscussionPeer: Peer? let members: PeerInfoMembersData? - let storyListContext: PeerStoryListContext? - let storyArchiveListContext: PeerStoryListContext? + let storyListContext: StoryListContext? + let storyArchiveListContext: StoryListContext? let encryptionKeyFingerprint: SecretChatKeyFingerprint? let globalSettings: TelegramGlobalSettings? let invitations: PeerExportedInvitationsState? @@ -352,6 +352,8 @@ final class PeerInfoScreenData { let isPremiumRequiredForStoryPosting: Bool let personalChannel: PeerInfoPersonalChannelData? let starsState: StarsContext.State? + let starsRevenueStatsState: StarsRevenueStats? + let starsRevenueStatsContext: StarsRevenueStatsContext? let _isContact: Bool var forceIsContact: Bool = false @@ -378,8 +380,8 @@ final class PeerInfoScreenData { groupsInCommon: GroupsInCommonContext?, linkedDiscussionPeer: Peer?, members: PeerInfoMembersData?, - storyListContext: PeerStoryListContext?, - storyArchiveListContext: PeerStoryListContext?, + storyListContext: StoryListContext?, + storyArchiveListContext: StoryListContext?, encryptionKeyFingerprint: SecretChatKeyFingerprint?, globalSettings: TelegramGlobalSettings?, invitations: PeerExportedInvitationsState?, @@ -392,7 +394,9 @@ final class PeerInfoScreenData { hasSavedMessageTags: Bool, isPremiumRequiredForStoryPosting: Bool, personalChannel: PeerInfoPersonalChannelData?, - starsState: StarsContext.State? + starsState: StarsContext.State?, + starsRevenueStatsState: StarsRevenueStats?, + starsRevenueStatsContext: StarsRevenueStatsContext? ) { self.peer = peer self.chatPeer = chatPeer @@ -422,6 +426,8 @@ final class PeerInfoScreenData { self.isPremiumRequiredForStoryPosting = isPremiumRequiredForStoryPosting self.personalChannel = personalChannel self.starsState = starsState + self.starsRevenueStatsState = starsRevenueStatsState + self.starsRevenueStatsContext = starsRevenueStatsContext } } @@ -638,7 +644,7 @@ private func peerInfoPersonalChannel(context: AccountContext, peerId: EnginePeer ) return combineLatest( - context.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: channelPeer.id, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 10, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: []), + context.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: channelPeer.id, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: 10, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: []), context.engine.data.subscribe( TelegramEngine.EngineData.Item.Peer.StoryStats(id: channelPeer.id) ), @@ -915,7 +921,9 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, hasSavedMessageTags: false, isPremiumRequiredForStoryPosting: true, personalChannel: personalChannel, - starsState: starsState + starsState: starsState, + starsRevenueStatsState: nil, + starsRevenueStatsContext: nil ) } } @@ -955,7 +963,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessageTags: false, isPremiumRequiredForStoryPosting: true, personalChannel: nil, - starsState: nil + starsState: nil, + starsRevenueStatsState: nil, + starsRevenueStatsContext: nil )) case let .user(userPeerId, secretChatId, kind): let groupsInCommon: GroupsInCommonContext? @@ -1074,7 +1084,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen |> distinctUntilChanged let hasStoryArchive: Signal - var storyArchiveListContext: PeerStoryListContext? + var storyArchiveListContext: StoryListContext? if isMyProfile { let storyArchiveListContextValue = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: true) storyArchiveListContext = storyArchiveListContextValue @@ -1172,6 +1182,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessageTags = .single(false) } + let starsRevenueStatsContextPromise = Promise(nil) + let starsRevenueStatsStatePromise = Promise(nil) + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: isMyProfile, chatLocationContextHolder: chatLocationContextHolder), @@ -1186,11 +1199,12 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessages, hasSavedMessageTags, peerInfoPersonalChannel(context: context, peerId: peerId, isSettings: false), - privacySettings + privacySettings, + starsRevenueStatsContextPromise.get(), + starsRevenueStatsStatePromise.get() ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, personalChannel, privacySettings -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, personalChannel, privacySettings, currentStarsRevenueStatsContext, starsRevenueStatsState -> PeerInfoScreenData in var availablePanes = availablePanes - if isMyProfile { availablePanes?.insert(.stories, at: 0) if let hasStoryArchive, hasStoryArchive { @@ -1255,6 +1269,14 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen enableQRLogin: false) } + if case .bot = kind, let user = peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { + if currentStarsRevenueStatsContext == nil { + let starsRevenueStatsContext = StarsRevenueStatsContext(account: context.account, peerId: peerId) + starsRevenueStatsContextPromise.set(.single(starsRevenueStatsContext)) + starsRevenueStatsStatePromise.set(starsRevenueStatsContext.state |> map { $0.stats }) + } + } + return PeerInfoScreenData( peer: peer, chatPeer: peerView.peers[peerId], @@ -1283,7 +1305,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessageTags: hasSavedMessageTags, isPremiumRequiredForStoryPosting: false, personalChannel: personalChannel, - starsState: nil + starsState: nil, + starsRevenueStatsState: starsRevenueStatsState, + starsRevenueStatsContext: currentStarsRevenueStatsContext ) } case .channel: @@ -1455,7 +1479,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessageTags: hasSavedMessageTags, isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting, personalChannel: nil, - starsState: nil + starsState: nil, + starsRevenueStatsState: nil, + starsRevenueStatsContext: nil ) } case let .group(groupId): @@ -1559,7 +1585,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen let requestsContextPromise = Promise(nil) let requestsStatePromise = Promise(nil) - let storyListContext: PeerStoryListContext? + let storyListContext: StoryListContext? let hasStories: Signal if peerId.namespace == Namespaces.Peer.CloudChannel { storyListContext = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false) @@ -1750,7 +1776,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessageTags: hasSavedMessageTags, isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting, personalChannel: nil, - starsState: nil + starsState: nil, + starsRevenueStatsState: nil, + starsRevenueStatsContext: nil )) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 79ed70241b2..9776e0ddcbc 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -555,7 +555,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { } transition.updateAlpha(node: self.regularContentNode, alpha: (state.isEditing || self.customNavigationContentNode != nil) ? 0.0 : 1.0) - transition.updateAlpha(node: self.navigationButtonContainer, alpha: self.customNavigationContentNode != nil ? 0.0 : 1.0) + if self.navigationTransition == nil { + transition.updateAlpha(node: self.navigationButtonContainer, alpha: self.customNavigationContentNode != nil ? 0.0 : 1.0) + } self.editingContentNode.alpha = state.isEditing ? 1.0 : 0.0 @@ -796,7 +798,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { } let iconSize = self.titleCredibilityIconView.update( - transition: Transition(navigationTransition), + transition: ComponentTransition(navigationTransition), component: AnyComponent(EmojiStatusComponent( context: self.context, animationCache: self.animationCache, @@ -852,7 +854,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { containerSize: CGSize(width: 34.0, height: 34.0) ) let expandedIconSize = self.titleExpandedCredibilityIconView.update( - transition: Transition(navigationTransition), + transition: ComponentTransition(navigationTransition), component: AnyComponent(EmojiStatusComponent( context: self.context, animationCache: self.animationCache, @@ -904,7 +906,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { } let iconSize = self.titleVerifiedIconView.update( - transition: Transition(navigationTransition), + transition: ComponentTransition(navigationTransition), component: AnyComponent(EmojiStatusComponent( context: self.context, animationCache: self.animationCache, @@ -919,7 +921,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { containerSize: CGSize(width: 34.0, height: 34.0) ) let expandedIconSize = self.titleExpandedVerifiedIconView.update( - transition: Transition(navigationTransition), + transition: ComponentTransition(navigationTransition), component: AnyComponent(EmojiStatusComponent( context: self.context, animationCache: self.animationCache, @@ -2125,7 +2127,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { } let backgroundCoverSize = self.backgroundCover.update( - transition: Transition(transition), + transition: ComponentTransition(transition), component: AnyComponent(PeerInfoCoverComponent( context: self.context, peer: peer.flatMap(EnginePeer.init), diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 5983f32a6bd..447b807ec82 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -419,7 +419,7 @@ private final class PeerInfoPendingPane { } } - let visualPaneNode = PeerInfoStoryPaneNode(context: context, peerId: peerId, contentType: .photoOrVideo, captureProtected: captureProtected, isSaved: false, isArchive: key == .storyArchive, isProfileEmbedded: true, canManageStories: canManage, navigationController: chatControllerInteraction.navigationController, listContext: key == .storyArchive ? data.storyArchiveListContext : data.storyListContext) + let visualPaneNode = PeerInfoStoryPaneNode(context: context, scope: .peer(id: peerId, isSaved: false, isArchived: key == .storyArchive), captureProtected: captureProtected, isProfileEmbedded: true, canManageStories: canManage, navigationController: chatControllerInteraction.navigationController, listContext: key == .storyArchive ? data.storyArchiveListContext : data.storyListContext) paneNode = visualPaneNode visualPaneNode.openCurrentDate = { openMediaCalendar() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index f71c23a5af1..bf73590b15a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -124,6 +124,7 @@ import GroupStickerPackSetupController import PeerNameColorItem import PeerSelectionScreen import UIKitRuntimeUtils +import OldChannelsController public enum PeerInfoAvatarEditingMode { case generic @@ -585,6 +586,7 @@ private final class PeerInfoInteraction { let editingOpenNameColorSetup: () -> Void let editingOpenInviteLinksSetup: () -> Void let editingOpenDiscussionGroupSetup: () -> Void + let editingOpenStars: () -> Void let editingToggleMessageSignatures: (Bool) -> Void let openParticipantsSection: (PeerInfoParticipantsSection) -> Void let openRecentActions: () -> Void @@ -656,6 +658,7 @@ private final class PeerInfoInteraction { editingOpenNameColorSetup: @escaping () -> Void, editingOpenInviteLinksSetup: @escaping () -> Void, editingOpenDiscussionGroupSetup: @escaping () -> Void, + editingOpenStars: @escaping () -> Void, editingToggleMessageSignatures: @escaping (Bool) -> Void, openParticipantsSection: @escaping (PeerInfoParticipantsSection) -> Void, openRecentActions: @escaping () -> Void, @@ -726,6 +729,7 @@ private final class PeerInfoInteraction { self.editingOpenNameColorSetup = editingOpenNameColorSetup self.editingOpenInviteLinksSetup = editingOpenInviteLinksSetup self.editingOpenDiscussionGroupSetup = editingOpenDiscussionGroupSetup + self.editingOpenStars = editingOpenStars self.editingToggleMessageSignatures = editingToggleMessageSignatures self.openParticipantsSection = openParticipantsSection self.openRecentActions = openRecentActions @@ -983,18 +987,7 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p items[.nicegramWallet]?.append(PeerInfoScreenHeaderItem(id: 0, text: "Multichain, Non-custodial")) items[.nicegramWallet]!.append(PeerInfoScreenDisclosureItem(id: 1, text: "Nicegram Wallet", icon: PresentationResourcesSettings.nicegramIcon, action: { Task { @MainActor in - let getCurrentUserUseCase = RepoUserContainer.shared.getCurrentUserUseCase() - let getCurrentWalletUseCase = WalletStorageModule.shared.getCurrentWalletUseCase() - - if getCurrentUserUseCase.isAuthorized(), - await getCurrentWalletUseCase() != nil { - let verificationManager = SecurityModule.shared.verificationManager() - verificationManager.doAfterVerification { - WalletEntryPoints.openHome() - } - } else { - AssistantUITgHelper.routeToAssistant(source: .generic) - } + WalletEntryPoints.openHome() } })) } @@ -1905,17 +1898,24 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL let ItemInfo = 3 let ItemDelete = 4 let ItemUsername = 5 + let ItemStars = 6 - let ItemIntro = 6 - let ItemCommands = 7 - let ItemBotSettings = 8 - let ItemBotInfo = 9 + let ItemIntro = 7 + let ItemCommands = 8 + let ItemBotSettings = 9 + let ItemBotInfo = 10 if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { - items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text("@\(user.addressName ?? "")"), text: presentationData.strings.PeerInfo_BotLinks, icon: nil, action: { + items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text("@\(user.addressName ?? "")"), text: presentationData.strings.PeerInfo_Bot_Username, icon: PresentationResourcesSettings.bot, action: { interaction.editingOpenPublicLinkSetup() })) + if let starsRevenueStats = data.starsRevenueStatsState, starsRevenueStats.balances.overallRevenue > 0 { + items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemStars, label: .text(presentationData.strings.PeerInfo_Bot_Balance_Stars(Int32(starsRevenueStats.balances.currentBalance))), text: presentationData.strings.PeerInfo_Bot_Balance, icon: PresentationResourcesSettings.stars, action: { + interaction.editingOpenStars() + })) + } + items[.peerSettings]!.append(PeerInfoScreenActionItem(id: ItemIntro, text: presentationData.strings.PeerInfo_Bot_EditIntro, icon: UIImage(bundleImageName: "Peer Info/BotIntro"), action: { interaction.openPeerMention("botfather", .withBotStartPayload(ChatControllerInitialBotStart(payload: "\(user.addressName ?? "")-intro", behavior: .interactive))) })) @@ -2834,6 +2834,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro editingOpenDiscussionGroupSetup: { [weak self] in self?.editingOpenDiscussionGroupSetup() }, + editingOpenStars: { [weak self] in + self?.editingOpenStars() + }, editingToggleMessageSignatures: { [weak self] value in self?.editingToggleMessageSignatures(value: value) }, @@ -3489,7 +3492,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro default: break } - }, openCheckoutOrReceipt: { _ in + }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in @@ -3546,7 +3549,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in }, openStickerEditor: { - }, openPhoneContextMenu: { _ in }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in @@ -3593,7 +3595,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } let presentationData = strongSelf.presentationData - let chatController = strongSelf.context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) let items: [ContextMenuItem] if recommended { @@ -5497,13 +5499,23 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let controller = self.controller else { return } + + if let navigationController = controller.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer { + for controller in minimizedContainer.controllers { + if let controller = controller as? AttachmentController, let mainController = controller.mainController as? WebAppController, mainController.botId == bot.peer.id && mainController.source == .settings { + navigationController.maximizeViewController(controller, animated: true) + return + } + } + } + let presentationData = self.presentationData let proceed: (Bool) -> Void = { [weak self] installed in guard let self else { return } - let params = WebAppParameters(source: .settings, peerId: self.context.account.peerId, botId: bot.peer.id, botName: bot.peer.compactDisplayTitle, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: bot.flags.contains(.hasSettings)) + let params = WebAppParameters(source: .settings, peerId: self.context.account.peerId, botId: bot.peer.id, botName: bot.peer.compactDisplayTitle, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: bot.flags.contains(.hasSettings), fullSize: true) let controller = standaloneWebAppController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, params: params, threadId: nil, openUrl: { [weak self] url, concealed, commit in self?.openUrl(url: url, concealed: concealed, external: false, forceExternal: true, commit: commit) }, requestSwitchInline: { _, _, _ in @@ -8626,6 +8638,13 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.controller?.push(channelDiscussionGroupSetupController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id)) } + private func editingOpenStars() { + guard let revenueContext = self.data?.starsRevenueStatsContext else { + return + } + self.controller?.push(self.context.sharedContext.makeStarsStatisticsScreen(context: self.context, peerId: self.peerId, revenueContext: revenueContext)) + } + private func editingOpenReactionsSetup() { guard let data = self.data, let peer = data.peer else { return @@ -8790,7 +8809,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let context = self.context let presentationData = self.presentationData - let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), address: location.address, provider: nil, id: nil, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, venue: MapVenue(title: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), address: location.address, provider: nil, id: nil, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) let controllerParams = LocationViewParams(sendLiveLocation: { _ in }, stopLiveLocation: { _ in @@ -9969,7 +9988,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } if !foundController { - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId), subject: nil, botStart: nil, mode: .standard(.default)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) chatController.hintPlayNextOutgoingGift() controllers.append(chatController) } @@ -10256,7 +10275,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } self.supportPeerDisposable.set((supportPeer.get() |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak self] peerId in if let strongSelf = self, let peerId = peerId { - push(strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default))) + push(strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)) } })) })]), in: .window(.root)) @@ -10787,7 +10806,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro proceed(chatController) }) } else { - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .none, botStart: nil, mode: .standard(.default)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .none, botStart: nil, mode: .standard(.default), params: nil) proceed(chatController) } } @@ -11253,7 +11272,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro )) }))) - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId), subject: .message(id: .id(index.id), highlight: nil, timecode: nil), botStart: nil, mode: .standard(.previewing)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId), subject: .message(id: .id(index.id), highlight: nil, timecode: nil), botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.controller?.presentInGlobalOverlay(contextController) @@ -11404,7 +11423,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let headerInset = sectionInset - let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: additive, animateHeader: transition.isAnimated) + let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: self.headerNode.navigationTransition == nil ? transition : .immediate, additive: additive, animateHeader: transition.isAnimated && self.headerNode.navigationTransition == nil) let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: headerHeight)) if additive { transition.updateFrameAdditive(node: self.headerNode, frame: headerFrame) @@ -11777,7 +11796,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let headerInset = sectionInset - let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: additive, animateHeader: animateHeader) + let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: self.headerNode.navigationTransition == nil ? transition : .immediate, additive: additive, animateHeader: animateHeader && self.headerNode.navigationTransition == nil) } let paneAreaExpansionDistance: CGFloat = 32.0 @@ -12291,8 +12310,9 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.chatLocation = .peer(id: peerId) } - if isSettings { - self.starsContext = context.starsContext + if isSettings, let starsContext = context.starsContext { + self.starsContext = starsContext + starsContext.load(force: true) } else { self.starsContext = nil } @@ -12989,7 +13009,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc }) { let _ = navigationController.popToViewController(chatController, animated: false) } else { - let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: context.account.peerId), subject: nil, botStart: nil, mode: .standard(.default)) + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: context.account.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) navigationController.replaceController(sourceController, with: chatController, animated: false) } @@ -13307,7 +13327,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig } let headerInset = sectionInset - topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.savedMessagesPeer ?? self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, threadData: self.screenNode.data?.threadData, peerNotificationSettings: self.screenNode.data?.peerNotificationSettings, threadNotificationSettings: self.screenNode.data?.threadNotificationSettings, globalNotificationSettings: self.screenNode.data?.globalNotificationSettings, statusData: self.screenNode.data?.status, panelStatusData: (nil, nil, nil), isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: false, animateHeader: true) + topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.savedMessagesPeer ?? self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, threadData: self.screenNode.data?.threadData, peerNotificationSettings: self.screenNode.data?.peerNotificationSettings, threadNotificationSettings: self.screenNode.data?.threadNotificationSettings, globalNotificationSettings: self.screenNode.data?.globalNotificationSettings, statusData: self.screenNode.data?.status, panelStatusData: (nil, nil, nil), isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: false, animateHeader: false) } let titleScale = (fraction * previousTitleNode.view.bounds.height + (1.0 - fraction) * self.headerNode.titleNodeRawContainer.bounds.height) / previousTitleNode.view.bounds.height diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD index a41fa15c29c..a619f335403 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD @@ -28,6 +28,8 @@ swift_library( "//submodules/TelegramUI/Components/MoreHeaderButton", "//submodules/TelegramUI/Components/MediaEditorScreen", "//submodules/SaveToCameraRoll", + "//submodules/ShareController", + "//submodules/OpenInExternalAppUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index 290b7a26204..e9fccb53a37 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -306,7 +306,7 @@ final class PeerInfoStoryGridScreenComponent: Component { } private var isUpdating = false - func update(component: PeerInfoStoryGridScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PeerInfoStoryGridScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -455,11 +455,8 @@ final class PeerInfoStoryGridScreenComponent: Component { } else { paneNode = PeerInfoStoryPaneNode( context: component.context, - peerId: component.peerId, - contentType: .photoOrVideo, + scope: .peer(id: component.peerId, isSaved: true, isArchived: component.scope == .archive), captureProtected: false, - isSaved: true, - isArchive: component.scope == .archive, isProfileEmbedded: false, canManageStories: true, navigationController: { [weak self] in @@ -520,7 +517,7 @@ final class PeerInfoStoryGridScreenComponent: Component { self.selectedCount = selectedIds.count if applyState { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } (self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle() }) @@ -551,7 +548,7 @@ final class PeerInfoStoryGridScreenComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift index 7a8cde1f26c..6bdc9c25811 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift @@ -16,29 +16,27 @@ import UndoUI import MoreHeaderButton import MediaEditorScreen import SaveToCameraRoll +import ShareController +import OpenInExternalAppUI final class StorySearchGridScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let searchQuery: String + let scope: StorySearchControllerScope + let listContext: SearchStoryListContext? init( context: AccountContext, - searchQuery: String + scope: StorySearchControllerScope, + listContext: SearchStoryListContext? ) { self.context = context - self.searchQuery = searchQuery + self.scope = scope + self.listContext = listContext } static func ==(lhs: StorySearchGridScreenComponent, rhs: StorySearchGridScreenComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.searchQuery != rhs.searchQuery { - return false - } - return true } @@ -71,7 +69,7 @@ final class StorySearchGridScreenComponent: Component { } private var isUpdating = false - func update(component: StorySearchGridScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StorySearchGridScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -99,14 +97,18 @@ final class StorySearchGridScreenComponent: Component { if let current = self.paneNode { paneNode = current } else { + let paneNodeScope: PeerInfoStoryPaneNode.Scope + switch component.scope { + case let .query(query): + paneNodeScope = .search(query: query) + case let .location(coordinates, venue): + paneNodeScope = .location(coordinates: coordinates, venue: venue) + } + paneNode = PeerInfoStoryPaneNode( context: component.context, - peerId: nil, - searchQuery: component.searchQuery, - contentType: .photoOrVideo, + scope: paneNodeScope, captureProtected: false, - isSaved: false, - isArchive: false, isProfileEmbedded: false, canManageStories: false, navigationController: { [weak self] in @@ -115,8 +117,9 @@ final class StorySearchGridScreenComponent: Component { } return self.environment?.controller()?.navigationController as? NavigationController }, - listContext: nil + listContext: component.listContext ) + paneNode.parentController = environment.controller() paneNode.isEmptyUpdated = { [weak self] _ in guard let self else { return @@ -164,28 +167,38 @@ final class StorySearchGridScreenComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } -public class StorySearchGridScreen: ViewControllerComponentContainer { +public final class StorySearchGridScreen: ViewControllerComponentContainer { private let context: AccountContext - private let searchQuery: String + private let scope: StorySearchControllerScope private var isDismissed: Bool = false private var titleView: ChatTitleView? + override public var additionalNavigationBarHeight: CGFloat { + if let componentView = self.node.hostView.componentView as? StorySearchGridScreenComponent.View, let paneNode = componentView.paneNode { + return paneNode.additionalNavigationHeight + } else { + return 0.0 + } + } + public init( context: AccountContext, - searchQuery: String + scope: StorySearchControllerScope, + listContext: SearchStoryListContext? = nil ) { self.context = context - self.searchQuery = searchQuery + self.scope = scope super.init(context: context, component: StorySearchGridScreenComponent( context: context, - searchQuery: searchQuery + scope: scope, + listContext: listContext ), navigationBarAppearance: .default, theme: .default) let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) @@ -203,6 +216,10 @@ public class StorySearchGridScreen: ViewControllerComponentContainer { self.navigationItem.titleView = self.titleView + if case .location = scope { + self.navigationItem.setRightBarButton(UIBarButtonItem(image: PresentationResourcesRootController.navigationShareIcon(presentationData.theme), style: .plain, target: self, action: #selector(self.sharePressed)), animated: true) + } + self.updateTitle() self.scrollToTop = { [weak self] in @@ -220,9 +237,30 @@ public class StorySearchGridScreen: ViewControllerComponentContainer { deinit { } + @objc private func sharePressed() { + guard case let .location(_, venue) = self.scope else { + return + } + let locationMap = TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, venue: nil, address: venue.address, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + + let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }) + + let shareAction = OpenInControllerAction(title: presentationData.strings.Conversation_ContextMenuShare, action: { [weak self] in + guard let self else { + return + } + self.present(ShareController(context: self.context, subject: .mapMedia(locationMap), externalShare: true), in: .window(.root), with: nil) + }) + self.present(OpenInActionSheetController(context: self.context, updatedPresentationData: nil, item: .location(location: locationMap, directions: nil), additionalAction: shareAction, openUrl: { [weak self] url in + guard let self else { + return + } + self.context.sharedContext.applicationBindings.openUrl(url) + }), in: .window(.root), with: nil) + } + func updateTitle() { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let _ = presentationData guard let componentView = self.node.hostView.componentView as? StorySearchGridScreenComponent.View, let paneNode = componentView.paneNode else { return @@ -235,8 +273,12 @@ public class StorySearchGridScreen: ViewControllerComponentContainer { } else { title = nil } - //TODO:localize - self.titleView?.titleContent = .custom("\(self.searchQuery)", title, false) + switch self.scope { + case let .query(query): + self.titleView?.titleContent = .custom("\(query)", title, false) + case .location: + self.titleView?.titleContent = .custom(presentationData.strings.StoryGridScreen_TitleLocationSearch, nil, false) + } } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD index b3c0e5015eb..973f424075c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD @@ -44,6 +44,8 @@ swift_library( "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/Components/ComponentDisplayAdapters", "//submodules/TelegramUI/Components/MediaEditorScreen", + "//submodules/LocationUI", + "//submodules/Components/MultilineTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 3a5051bada6..e254ffd8911 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -35,6 +35,12 @@ import PlainButtonComponent import ComponentDisplayAdapters import MediaEditorScreen import AvatarNode +import LocationUI +import CoreLocation +import Geocoding +import ItemListUI +import MultilineTextComponent +import LocationUI private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6) private let mediaBadgeTextColor = UIColor.white @@ -59,7 +65,7 @@ private final class VisualMediaItemInteraction { } private final class VisualMediaHoleAnchor: SparseItemGrid.HoleAnchor { - let storyId: Int32 + let storyId: StoryId override var id: AnyHashable { return AnyHashable(self.storyId) } @@ -74,7 +80,7 @@ private final class VisualMediaHoleAnchor: SparseItemGrid.HoleAnchor { return self.localMonthTimestamp } - init(index: Int, storyId: Int32, localMonthTimestamp: Int32) { + init(index: Int, storyId: StoryId, localMonthTimestamp: Int32) { self.indexValue = index self.storyId = storyId self.localMonthTimestamp = localMonthTimestamp @@ -88,12 +94,13 @@ private final class VisualMediaItem: SparseItemGrid.Item { } let localMonthTimestamp: Int32 let peer: PeerReference + let storyId: StoryId let story: EngineStoryItem let authorPeer: EnginePeer? let isPinned: Bool override var id: AnyHashable { - return AnyHashable(self.story.id) + return AnyHashable(self.storyId) } override var tag: Int32 { @@ -101,12 +108,13 @@ private final class VisualMediaItem: SparseItemGrid.Item { } override var holeAnchor: SparseItemGrid.HoleAnchor { - return VisualMediaHoleAnchor(index: self.index, storyId: self.story.id, localMonthTimestamp: self.localMonthTimestamp) + return VisualMediaHoleAnchor(index: self.index, storyId: self.storyId, localMonthTimestamp: self.localMonthTimestamp) } - init(index: Int, peer: PeerReference, story: EngineStoryItem, authorPeer: EnginePeer?, isPinned: Bool, localMonthTimestamp: Int32) { + init(index: Int, peer: PeerReference, storyId: StoryId, story: EngineStoryItem, authorPeer: EnginePeer?, isPinned: Bool, localMonthTimestamp: Int32) { self.indexValue = index self.peer = peer + self.storyId = storyId self.story = story self.authorPeer = authorPeer self.isPinned = isPinned @@ -435,7 +443,7 @@ private final class DurationLayer: SimpleLayer { avatarLayer.contents = other.avatarLayer?.contents } - func update(directMediaImageCache: DirectMediaImageCache, author: EnginePeer, synchronous: SparseItemGrid.Synchronous) { + func update(directMediaImageCache: DirectMediaImageCache, author: EnginePeer, constrainedWidth: CGFloat, synchronous: SparseItemGrid.Synchronous) { let avatarLayer: SimpleLayer if let current = self.avatarLayer { avatarLayer = current @@ -451,7 +459,7 @@ private final class DurationLayer: SimpleLayer { if self.authorPeerId != author.id { let string = NSAttributedString(string: author.debugDisplayTitle, font: durationFont, textColor: .white) - let bounds = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + let bounds = string.boundingRect(with: CGSize(width: constrainedWidth - 24.0, height: 20.0), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil) let textSize = CGSize(width: ceil(bounds.width), height: ceil(bounds.height)) let sideInset: CGFloat = 6.0 let verticalInset: CGFloat = 2.0 @@ -463,7 +471,7 @@ private final class DurationLayer: SimpleLayer { context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.22).cgColor) UIGraphicsPushContext(context) - string.draw(in: bounds.offsetBy(dx: sideInset, dy: verticalInset)) + string.draw(with: bounds.offsetBy(dx: sideInset, dy: verticalInset), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil) UIGraphicsPopContext() }) self.contents = image?.cgImage @@ -498,6 +506,26 @@ private final class DurationLayer: SimpleLayer { } private final class ItemLayer: CALayer, SparseItemGridLayer { + struct Params: Equatable { + let size: CGSize + let viewCount: Int32? + let duration: Int32? + let topRightIcon: ItemTopRightIcon? + let authorId: EnginePeer.Id? + let isMin: Bool + let minFactor: CGFloat + + init(size: CGSize, viewCount: Int32?, duration: Int32?, topRightIcon: ItemTopRightIcon?, authorId: EnginePeer.Id?, isMin: Bool, minFactor: CGFloat) { + self.size = size + self.viewCount = viewCount + self.duration = duration + self.topRightIcon = topRightIcon + self.authorId = authorId + self.isMin = isMin + self.minFactor = minFactor + } + } + var item: VisualMediaItem? var viewCountLayer: DurationLayer? var durationLayer: DurationLayer? @@ -511,6 +539,8 @@ private final class ItemLayer: CALayer, SparseItemGridLayer { var selectionLayer: GridMessageSelectionLayer? var dustLayer: MediaDustLayer? var disposable: Disposable? + + var currentParams: Params? var hasContents: Bool = false @@ -552,7 +582,13 @@ private final class ItemLayer: CALayer, SparseItemGridLayer { self.item = item } - func updateDuration(viewCount: Int32?, duration: Int32?, topRightIcon: ItemTopRightIcon?, author: EnginePeer?, isMin: Bool, minFactor: CGFloat, directMediaImageCache: DirectMediaImageCache, synchronous: SparseItemGrid.Synchronous) { + func updateDuration(size: CGSize, viewCount: Int32?, duration: Int32?, topRightIcon: ItemTopRightIcon?, author: EnginePeer?, isMin: Bool, minFactor: CGFloat, directMediaImageCache: DirectMediaImageCache, synchronous: SparseItemGrid.Synchronous) { + let params = Params(size: size, viewCount: viewCount, duration: duration, topRightIcon: topRightIcon, authorId: author?.id, isMin: isMin, minFactor: minFactor) + if self.currentParams == params { + return + } + self.currentParams = params + self.minFactor = minFactor if let viewCount { @@ -607,11 +643,11 @@ private final class ItemLayer: CALayer, SparseItemGridLayer { if let author { if let authorLayer = self.authorLayer { - authorLayer.update(directMediaImageCache: directMediaImageCache, author: author, synchronous: synchronous) + authorLayer.update(directMediaImageCache: directMediaImageCache, author: author, constrainedWidth: size.width, synchronous: synchronous) } else { let authorLayer = DurationLayer() authorLayer.contentsGravity = .bottomLeft - authorLayer.update(directMediaImageCache: directMediaImageCache, author: author, synchronous: synchronous) + authorLayer.update(directMediaImageCache: directMediaImageCache, author: author, constrainedWidth: size.width, synchronous: synchronous) self.addSublayer(authorLayer) authorLayer.frame = CGRect(origin: CGPoint(x: 17.0, y: 3.0), size: CGSize()) authorLayer.transform = CATransform3DMakeScale(minFactor, minFactor, 1.0) @@ -965,7 +1001,7 @@ private final class ItemTransitionView: UIView { fatalError("init(coder:) has not been implemented") } - func update(state: StoryContainerScreen.TransitionState, transition: Transition) { + func update(state: StoryContainerScreen.TransitionState, transition: ComponentTransition) { let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress) if let copyDurationLayer = self.copyDurationLayer, let durationLayerBottomLeftPosition = self.durationLayerBottomLeftPosition { @@ -1064,6 +1100,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { var onTagTapImpl: (() -> Void)? var didScrollImpl: (() -> Void)? var coveringInsetOffsetUpdatedImpl: ((ContainedViewLayoutTransition) -> Void)? + var scrollingOffsetUpdatedImpl: ((ContainedViewLayoutTransition) -> Void)? var onBeginFastScrollingImpl: (() -> Void)? var getShimmerColorsImpl: (() -> SparseItemGrid.ShimmerColors)? var updateShimmerLayersImpl: ((SparseItemGridDisplayItem) -> Void)? @@ -1166,7 +1203,8 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { if let result = directMediaImageCache.getImage(peer: item.peer, story: story, media: selectedMedia, width: imageWidthSpec, aspectRatio: 0.81, possibleWidths: SparseItemGridBindingImpl.widthSpecs.1, includeBlurred: hasSpoiler || displayItem.blurLayer != nil, synchronous: synchronous == .full) { if let image = result.image { layer.setContents(image) - switch synchronous { + + /*switch synchronous { case .none: layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak self, weak layer, weak displayItem] _ in layer?.hasContents = true @@ -1176,7 +1214,8 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { }) default: layer.hasContents = true - } + }*/ + layer.hasContents = true } if let image = result.blurredImage { layer.setSpoilerContents(image) @@ -1195,13 +1234,16 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { return } let deltaTime = CFAbsoluteTimeGetCurrent() - startTimestamp - let synchronousValue: Bool + var synchronousValue: Bool switch synchronous { case .none, .full: synchronousValue = false case .semi: synchronousValue = deltaTime < 0.1 } + if "".isEmpty { + synchronousValue = true + } if let contents = layer.getContents(), !synchronousValue { let copyLayer = ItemLayer() @@ -1292,7 +1334,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { isMin = layer.bounds.width < 80.0 } - layer.updateDuration(viewCount: viewCount, duration: duration, topRightIcon: topRightIcon, author: item.authorPeer, isMin: isMin, minFactor: min(1.0, layer.bounds.height / 74.0), directMediaImageCache: self.directMediaImageCache, synchronous: synchronous) + layer.updateDuration(size: layer.bounds.size, viewCount: viewCount, duration: duration, topRightIcon: topRightIcon, author: item.authorPeer, isMin: isMin, minFactor: min(1.0, layer.bounds.height / 74.0), directMediaImageCache: self.directMediaImageCache, synchronous: synchronous) } func unbindLayer(layer: SparseItemGridLayer) { @@ -1333,6 +1375,10 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { func coveringInsetOffsetUpdated(transition: ContainedViewLayoutTransition) { self.coveringInsetOffsetUpdatedImpl?(transition) } + + func scrollingOffsetUpdated(transition: ContainedViewLayoutTransition) { + self.scrollingOffsetUpdatedImpl?(transition) + } func onBeginFastScrolling() { self.onBeginFastScrollingImpl?() @@ -1347,13 +1393,91 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { } } -public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScrollViewDelegate, ASGestureRecognizerDelegate { - public enum ContentType { - case photoOrVideo - case photo - case video +private final class StorySearchHeaderComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let count: Int + + init( + theme: PresentationTheme, + strings: PresentationStrings, + count: Int + ) { + self.theme = theme + self.strings = strings + self.count = count } + + static func ==(lhs: StorySearchHeaderComponent, rhs: StorySearchHeaderComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings != rhs.strings { + return false + } + if lhs.count != rhs.count { + return false + } + return true + } + + final class View: UIView { + private let title = ComponentView() + + private var component: StorySearchHeaderComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: StorySearchHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + if self.component?.theme !== component.theme { + self.backgroundColor = component.theme.chatList.sectionHeaderFillColor + } + + let insets = UIEdgeInsets(top: 7.0, left: 16.0, bottom: 7.0, right: 16.0) + + let titleString = component.strings.StoryList_GridHeaderLocationSearch(Int32(component.count)) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleString, font: Font.regular(13.0), textColor: component.theme.chatList.sectionHeaderTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - insets.left - insets.right, height: 100.0) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: titleSize) + } + + return CGSize(width: availableSize.width, height: titleSize.height + insets.top + insets.bottom) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} +public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScrollViewDelegate, ASGestureRecognizerDelegate { + public enum Scope { + case peer(id: EnginePeer.Id, isSaved: Bool, isArchived: Bool) + case search(query: String) + case location(coordinates: MediaArea.Coordinates, venue: MediaArea.Venue) + } + public struct ZoomLevel { fileprivate var value: SparseItemGrid.ZoomLevel @@ -1370,26 +1494,66 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } } + private struct MapInfoData: Equatable { + var location: TelegramMediaMap + var address: String? + var distance: Double? + var drivingTime: ExpectedTravelTime + var transitTime: ExpectedTravelTime + var walkingTime: ExpectedTravelTime + var hasEta: Bool + + init( + location: TelegramMediaMap, + address: String?, + distance: Double?, + drivingTime: ExpectedTravelTime, + transitTime: ExpectedTravelTime, + walkingTime: ExpectedTravelTime, + hasEta: Bool + ) { + self.location = location + self.address = address + self.distance = distance + self.drivingTime = drivingTime + self.transitTime = transitTime + self.walkingTime = walkingTime + self.hasEta = hasEta + } + } + private let context: AccountContext - private let peerId: PeerId? - private let searchQuery: String? - private let isSaved: Bool - private let isArchive: Bool + private let scope: Scope private let isProfileEmbedded: Bool private let canManageStories: Bool - public private(set) var contentType: ContentType - private var contentTypePromise: ValuePromise private let navigationController: () -> NavigationController? public weak var parentController: ViewController? private let contextGestureContainerNode: ContextControllerSourceNode + + private var mapOptionsNode: LocationOptionsNode? + private var mapNode: LocationMapHeaderNode? + private var mapDisposable: Disposable? + + private var locationViewState: LocationViewState = LocationViewState() { + didSet { + self.locationViewStatePromise.set(.single(self.locationViewState)) + } + } + private let locationViewStatePromise = Promise(LocationViewState()) + + private var mapInfoData: MapInfoData? + private var mapInfoNode: LocationInfoListItemNode? + private var searchHeader: ComponentView? + private let itemGrid: SparseItemGrid private let itemGridBinding: SparseItemGridBindingImpl private let directMediaImageCache: DirectMediaImageCache private var items: SparseItemGrid.Items? private var pinnedIds: Set = Set() + private var itemCount: Int? private var didUpdateItemsOnce: Bool = false private var selectionPanel: ComponentView? @@ -1468,7 +1632,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private var animationTimer: SwiftSignalKit.Timer? public private(set) var calendarSource: SparseMessageCalendar? - private var listSource: PeerStoryListContext + private var listSource: StoryListContext public var openCurrentDate: (() -> Void)? public var paneDidScroll: (() -> Void)? @@ -1484,26 +1648,33 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private weak var pendingOpenListContext: PeerStoryListContentContextImpl? - private var preloadArchiveListContext: PeerStoryListContext? + private var preloadArchiveListContext: StoryListContext? private var emptyStateView: ComponentView? private weak var contextControllerToDismissOnSelection: ContextControllerProtocol? private weak var tempContextContentItemNode: TempExtractedItemNode? + + public var additionalNavigationHeight: CGFloat { + if self.locationViewState.displayingMapModeOptions { + return 38.0 + } + return 0.0 + } - public init(context: AccountContext, peerId: PeerId?, searchQuery: String? = nil, contentType: ContentType, captureProtected: Bool, isSaved: Bool, isArchive: Bool, isProfileEmbedded: Bool, canManageStories: Bool, navigationController: @escaping () -> NavigationController?, listContext: PeerStoryListContext?) { + public init(context: AccountContext, scope: Scope, captureProtected: Bool, isProfileEmbedded: Bool, canManageStories: Bool, navigationController: @escaping () -> NavigationController?, listContext: StoryListContext?) { self.context = context - self.peerId = peerId - self.searchQuery = searchQuery - self.contentType = contentType - self.contentTypePromise = ValuePromise(contentType) + self.scope = scope self.navigationController = navigationController - self.isSaved = isSaved - self.isArchive = isArchive self.isProfileEmbedded = isProfileEmbedded self.canManageStories = canManageStories - self.isSelectionModeActive = !isProfileEmbedded && isArchive + switch scope { + case let .peer(_, _, isArchived): + self.isSelectionModeActive = !isProfileEmbedded && isArchived + default: + self.isSelectionModeActive = false + } self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } @@ -1518,12 +1689,23 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr displayPrivacy: isProfileEmbedded ) - self.listSource = listContext ?? PeerStoryListContext(account: context.account, peerId: peerId ?? context.account.peerId, isArchived: self.isArchive) + if let listContext { + self.listSource = listContext + } else { + switch self.scope { + case let .peer(id, _, isArchived): + self.listSource = PeerStoryListContext(account: context.account, peerId: id, isArchived: isArchived) + case let .search(query): + self.listSource = SearchStoryListContext(account: context.account, source: .hashtag(query)) + case let .location(coordinates, venue): + self.listSource = SearchStoryListContext(account: context.account, source: .mediaArea(.venue(coordinates: coordinates, venue: venue))) + } + } self.calendarSource = nil super.init() - if self.peerId != nil { + if case .peer = self.scope { let _ = (ApplicationSpecificNotice.getSharedMediaScrollingTooltip(accountManager: context.sharedContext.accountManager) |> deliverOnMainQueue).start(next: { [weak self] count in guard let strongSelf = self else { @@ -1570,12 +1752,18 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return } - //TODO:localize + var splitIndexIntoDays = true + switch self.scope { + case .peer: + break + default: + splitIndexIntoDays = false + } let listContext = PeerStoryListContentContextImpl( context: self.context, - peerId: self.peerId ?? self.context.account.peerId, listContext: self.listSource, - initialId: item.story.id + initialId: item.story.id, + splitIndexIntoDays: splitIndexIntoDays ) self.pendingOpenListContext = listContext self.itemGrid.isUserInteractionEnabled = false @@ -1617,7 +1805,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr ) if let blurLayer = foundItem?.blurLayer { - let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)) transition.setAlpha(layer: blurLayer, alpha: 0.0) } } @@ -1644,7 +1832,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } if let foundItemLayer { if let blurLayer = foundItem?.blurLayer { - let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)) transition.setAlpha(layer: blurLayer, alpha: 1.0) } @@ -1726,15 +1914,26 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return } strongSelf.paneDidScroll?() - strongSelf.cancelPreviewGestures() + + if strongSelf.locationViewState.displayingMapModeOptions { + strongSelf.locationViewState.displayingMapModeOptions = false + strongSelf.parentController?.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) + } } self.itemGridBinding.coveringInsetOffsetUpdatedImpl = { [weak self] transition in - guard let strongSelf = self else { + guard let self else { + return + } + self.tabBarOffsetUpdated?(transition) + } + + self.itemGridBinding.scrollingOffsetUpdatedImpl = { [weak self] transition in + guard let self else { return } - strongSelf.tabBarOffsetUpdated?(transition) + self.gridScrollingOffsetUpdated(transition: transition) } var processedOnBeginFastScrolling = false @@ -1835,7 +2034,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } } ) - //TODO:selection if self.isSelectionModeActive { self._itemInteraction?.selectedIds = Set() } @@ -1844,6 +2042,41 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.contextGestureContainerNode.isGestureEnabled = self.isProfileEmbedded self.contextGestureContainerNode.addSubnode(self.itemGrid) self.addSubnode(self.contextGestureContainerNode) + + if case .location = scope { + let mapNode = LocationMapHeaderNode( + presentationData: self.presentationData, + toggleMapModeSelection: { [weak self] in + guard let self else { + return + } + + var state = self.locationViewState + state.displayingMapModeOptions = !state.displayingMapModeOptions + self.locationViewState = state + }, + goToUserLocation: { [weak self] in + guard let self else { + return + } + + var state = self.locationViewState + state.displayingMapModeOptions = false + state.selectedLocation = .user + switch state.trackingMode { + case .none: + state.trackingMode = .follow + case .follow: + state.trackingMode = .followWithHeading + case .followWithHeading: + state.trackingMode = .none + } + self.locationViewState = state + } + ) + self.mapNode = mapNode + self.addSubnode(mapNode) + } self.contextGestureContainerNode.shouldBegin = { [weak self] point in guard let strongSelf = self else { @@ -1933,7 +2166,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr strongSelf.openContextMenu(item: story, itemLayer: itemLayer, rect: rect, gesture: gesture) } - self.statusPromise.set(.single(PeerInfoStatusData(text: "", isActivity: false, key: self.isArchive ? .storyArchive : .stories))) + let paneKey: PeerInfoPaneKey + switch self.scope { + case let .peer(_, _, isArchived): + paneKey = isArchived ? .storyArchive : .stories + default: + paneKey = .stories + } + self.statusPromise.set(.single(PeerInfoStatusData(text: "", isActivity: false, key: paneKey))) self.presentationDataDisposable = (self.context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in @@ -1942,15 +2182,149 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } strongSelf.itemGridBinding.updatePresentationData(presentationData: presentationData) - strongSelf.itemGrid.updatePresentationData(theme: presentationData.theme) + strongSelf.mapOptionsNode?.updatePresentationData(presentationData) }) self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: false) - if peerId == context.account.peerId && !isArchive { + if case let .peer(id, _, isArchived) = self.scope, id == context.account.peerId, !isArchived { self.preloadArchiveListContext = PeerStoryListContext(account: context.account, peerId: context.account.peerId, isArchived: true) } + + if case let .location(_, venue) = scope, let mapNode = self.mapNode { + let locationCoordinate = CLLocationCoordinate2D(latitude: venue.latitude, longitude: venue.longitude) + + var initialMapState = LocationViewState() + initialMapState.selectedLocation = .coordinate(locationCoordinate, true) + self.locationViewStatePromise.set(.single(initialMapState)) + + let userLocation: Signal = .single(nil) + |> then( + throttledUserLocation(mapNode.mapNode.userLocation) + ) + + var eta: Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> = .single((.calculating, .calculating, .calculating)) + var address: Signal = .single(nil) + + let locale = localeWithStrings(self.presentationData.strings) + eta = .single((.calculating, .calculating, .calculating)) + |> then(combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .automobile), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .transit), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .walking)) + |> mapToSignal { drivingTime, transitTime, walkingTime -> Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> in + if case .calculating = drivingTime { + return .complete() + } + if case .calculating = transitTime { + return .complete() + } + if case .calculating = walkingTime { + return .complete() + } + + return .single((drivingTime, transitTime, walkingTime)) + }) + + /*if let venue = location.venue, let venueAddress = venue.address, !venueAddress.isEmpty { + address = .single(venueAddress) + } else*/ do { + address = .single(nil) + |> then( + reverseGeocodeLocation(latitude: locationCoordinate.latitude, longitude: locationCoordinate.longitude, locale: locale) + |> map { placemark -> String? in + return placemark?.compactDisplayAddress ?? "" + } + ) + } + + let previousState = Atomic(value: nil) + let previousAnnotations = Atomic<[LocationPinAnnotation]>(value: []) + + self.mapDisposable = (combineLatest( + context.sharedContext.presentationData, + self.locationViewStatePromise.get(), + mapNode.mapNode.userLocation, + userLocation, + address, + eta + ) + |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, userLocation, distance, address, eta in + guard let self, let mapNode = self.mapNode else { + return + } + + let previousState = previousState.swap(state) + + var annotations: [LocationPinAnnotation] = [] + + let subjectLocation = CLLocation(latitude: locationCoordinate.latitude, longitude: locationCoordinate.longitude) + let distance = userLocation.flatMap { subjectLocation.distance(from: $0) } + + let locationMap = TelegramMediaMap(latitude: locationCoordinate.latitude, longitude: locationCoordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, address: venue.address, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + + let mapInfoData = MapInfoData( + location: locationMap, + address: address, + distance: distance, + drivingTime: eta.0, + transitTime: eta.1, + walkingTime: eta.2, + hasEta: false + ) + + annotations.append(LocationPinAnnotation(context: context, theme: presentationData.theme, location: locationMap, queryId: nil, resultId: nil, forcedSelection: true)) + + mapNode.updateState( + mapMode: state.mapMode, + trackingMode: state.trackingMode, + displayingMapModeOptions: state.displayingMapModeOptions, + displayingPlacesButton: false, + proximityNotification: nil, + animated: false + ) + + mapNode.mapNode.trackingMode = state.trackingMode + + let previousAnnotations = previousAnnotations.swap(annotations) + if annotations != previousAnnotations { + mapNode.mapNode.annotations = annotations + } + + switch state.selectedLocation { + case .initial: + if previousState?.selectedLocation != .initial { + mapNode.mapNode.setMapCenter(coordinate: locationCoordinate, span: LocationMapNode.viewMapSpan, animated: previousState != nil) + } + case let .coordinate(coordinate, defaultSpan): + if let previousState = previousState, case let .coordinate(previousCoordinate, _) = previousState.selectedLocation, locationCoordinatesAreEqual(previousCoordinate, coordinate) { + } else { + mapNode.mapNode.setMapCenter( + coordinate: coordinate, + span: defaultSpan ? LocationMapNode.defaultMapSpan : LocationMapNode.viewMapSpan, + animated: true + ) + } + case .user: + if previousState?.selectedLocation != .user, let userLocation = userLocation { + mapNode.mapNode.setMapCenter( + coordinate: userLocation.coordinate, + isUserLocation: true, + animated: true + ) + } + case .custom: + break + } + + if let previousState, previousState.displayingMapModeOptions != state.displayingMapModeOptions { + self.parentController?.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) + } else { + if self.mapInfoData != mapInfoData { + self.mapInfoData = mapInfoData + self.update(transition: .immediate) + } + } + }) + } } deinit { @@ -1959,6 +2333,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.animationTimer?.invalidate() self.presentationDataDisposable?.dispose() self.updateDisposable.dispose() + self.mapDisposable?.dispose() } public func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal { @@ -1985,24 +2360,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr var items: [ContextMenuItem] = [] - if canManage, let peerId = self.peerId { - items.append(.action(ContextMenuActionItem(text: !self.isArchive ? self.presentationData.strings.StoryList_ItemAction_Archive : self.presentationData.strings.StoryList_ItemAction_Unarchive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: self.isArchive ? "Chat/Context Menu/Archive" : "Chat/Context Menu/Unarchive"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + if canManage, case let .peer(peerId, _, isArchived) = self.scope { + items.append(.action(ContextMenuActionItem(text: !isArchived ? self.presentationData.strings.StoryList_ItemAction_Archive : self.presentationData.strings.StoryList_ItemAction_Unarchive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isArchived ? "Chat/Context Menu/Archive" : "Chat/Context Menu/Unarchive"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in guard let self else { f(.default) return } - if self.isArchive { + if isArchived { f(.default) } else { f(.dismissWithoutContent) } - let _ = self.context.engine.messages.updateStoriesArePinned(peerId: peerId, ids: [item.id: item], isPinned: self.isArchive ? true : false).startStandalone() - self.parentController?.present(UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: self.isArchive ? self.presentationData.strings.StoryList_ToastUnarchived_Text(1) : self.presentationData.strings.StoryList_ToastArchived_Text(1), cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + let _ = self.context.engine.messages.updateStoriesArePinned(peerId: peerId, ids: [item.id: item], isPinned: isArchived ? true : false).startStandalone() + self.parentController?.present(UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: isArchived ? self.presentationData.strings.StoryList_ToastUnarchived_Text(1) : self.presentationData.strings.StoryList_ToastArchived_Text(1), cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) }))) - if !self.isArchive { + if !isArchived { let isPinned = self.pinnedIds.contains(item.id) items.append(.action(ContextMenuActionItem(text: isPinned ? self.presentationData.strings.StoryList_ItemAction_Unpin : self.presentationData.strings.StoryList_ItemAction_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak itemLayer] _, f in itemLayer?.isHidden = false @@ -2098,7 +2473,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if !item.isForwardingDisabled, case .everyone = item.privacy?.base { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Forward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss(completion: { - guard let self, let peerId = self.peerId else { + guard let self, case let .peer(peerId, _, _) = self.scope else { return } @@ -2200,9 +2575,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr parentController.presentInGlobalOverlay(contextController) } - public func updateContentType(contentType: ContentType) { - } - public func updateZoomLevel(level: ZoomLevel) { self.itemGrid.setZoomLevel(level: level.value) @@ -2237,37 +2609,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr var firstTime = true let queue = Queue() - let authorPeer: Signal - if self.searchQuery != nil { - authorPeer = self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId) - ) - } else { - authorPeer = .single(nil) - } - - var state = self.listSource.state - if self.peerId == nil && self.listDisposable == nil { - state = .single(PeerStoryListContext.State( - peerReference: nil, - items: [], - pinnedIds: Set(), - totalCount: 0, - loadMoreToken: 0, - isCached: false, - hasCache: false, - allEntityFiles: [:] - )) |> then(state |> delay(2.0, queue: .mainQueue())) - } + let state = self.listSource.state self.listDisposable?.dispose() self.listDisposable = nil - self.listDisposable = (combineLatest( - state, - authorPeer - ) - |> deliverOn(queue)).startStrict(next: { [weak self] state, authorPeer in + let context = self.context + self.listDisposable = (state + |> deliverOn(queue)).startStrict(next: { [weak self] state in guard let self else { return } @@ -2275,36 +2624,54 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr let title: String if state.totalCount == 0 { title = "" - } else { - if self.isSaved { + } else if case let .peer(_, isSaved, isArchived) = self.scope { + if isSaved { title = self.presentationData.strings.StoryList_SubtitleSaved(Int32(state.totalCount)) - } else if self.isArchive { + } else if isArchived { title = self.presentationData.strings.StoryList_SubtitleArchived(Int32(state.totalCount)) } else { title = self.presentationData.strings.StoryList_SubtitleCount(Int32(state.totalCount)) } + } else { + title = "" + } + let paneKey: PeerInfoPaneKey + switch self.scope { + case let .peer(_, _, isArchived): + paneKey = isArchived ? .storyArchive : .stories + default: + paneKey = .stories } - self.statusPromise.set(.single(PeerInfoStatusData(text: title, isActivity: false, key: self.isArchive ? .storyArchive : .stories))) + self.statusPromise.set(.single(PeerInfoStatusData(text: title, isActivity: false, key: paneKey))) let timezoneOffset = Int32(TimeZone.current.secondsFromGMT()) var mappedItems: [SparseItemGrid.Item] = [] var mappedHoles: [SparseItemGrid.HoleAnchor] = [] var totalCount: Int = 0 - if let peerReference = state.peerReference { - for item in state.items { - mappedItems.append(VisualMediaItem( - index: mappedItems.count, - peer: peerReference, - story: item, - authorPeer: authorPeer, - isPinned: state.pinnedIds.contains(item.id), - localMonthTimestamp: Month(localTimestamp: item.timestamp + timezoneOffset).packedValue - )) + for item in state.items { + var peerReference: PeerReference? + if let value = state.peerReference { + peerReference = value + } else if let peer = item.peer { + peerReference = PeerReference(peer._asPeer()) } - if mappedItems.count < state.totalCount, let lastItem = state.items.last, let loadMoreToken = state.loadMoreToken { - mappedHoles.append(VisualMediaHoleAnchor(index: mappedItems.count, storyId: Int32(loadMoreToken), localMonthTimestamp: Month(localTimestamp: lastItem.timestamp + timezoneOffset).packedValue)) + guard let peerReference else { + continue } + + mappedItems.append(VisualMediaItem( + index: mappedItems.count, + peer: peerReference, + storyId: item.id, + story: item.storyItem, + authorPeer: item.peer, + isPinned: state.pinnedIds.contains(item.storyItem.id), + localMonthTimestamp: Month(localTimestamp: item.storyItem.timestamp + timezoneOffset).packedValue + )) + } + if mappedItems.count < state.totalCount, let lastItem = state.items.last, let _ = state.loadMoreToken { + mappedHoles.append(VisualMediaHoleAnchor(index: mappedItems.count, storyId: StoryId(peerId: context.account.peerId, id: Int32.max), localMonthTimestamp: Month(localTimestamp: lastItem.storyItem.timestamp + timezoneOffset).packedValue)) } totalCount = state.totalCount totalCount = max(mappedItems.count, totalCount) @@ -2319,8 +2686,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } var headerText: String? - if strongSelf.isArchive && !mappedItems.isEmpty && strongSelf.peerId == strongSelf.context.account.peerId { - headerText = strongSelf.presentationData.strings.StoryList_ArchiveDescription + if case let .peer(peerId, _, isArchived) = strongSelf.scope { + if isArchived && !mappedItems.isEmpty && peerId == strongSelf.context.account.peerId { + headerText = strongSelf.presentationData.strings.StoryList_ArchiveDescription + } } let items = SparseItemGrid.Items( @@ -2331,6 +2700,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr headerText: headerText, snapTopInset: false ) + + strongSelf.itemCount = state.totalCount let currentSynchronous = synchronous && firstTime let currentReloadAtTop = reloadAtTop && firstTime @@ -2342,16 +2713,20 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } private func updateHistory(items: SparseItemGrid.Items, pinnedIds: Set, synchronous: Bool, reloadAtTop: Bool) { + var transition: ContainedViewLayoutTransition = .immediate + if case .location = self.scope, let previousItems = self.items, previousItems.items.count == 0, previousItems.count != 0, items.items.count == 0, items.count == 0 { + transition = .animated(duration: 0.3, curve: .spring) + } + self.items = items self.pinnedIds = pinnedIds - self.isEmptyUpdated(self.isEmpty) if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { var gridSnapshot: UIView? if reloadAtTop { gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false) } - self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: .immediate) + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: transition) self.updateSelectedItems(animated: false) if let gridSnapshot = gridSnapshot { self.view.addSubview(gridSnapshot) @@ -2360,6 +2735,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr }) } } + + self.isEmptyUpdated(self.isEmpty) if !self.didSetReady { self.didSetReady = true @@ -2648,7 +3025,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr itemLayer.isHidden = itemHidden if let blurLayer = itemValue.blurLayer { - let transition = Transition.immediate + let transition = ComponentTransition.immediate if itemHidden { transition.setAlpha(layer: blurLayer, alpha: 0.0) } else { @@ -2659,7 +3036,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } private func presentDeleteConfirmation(ids: Set) { - guard let peerId = self.peerId else { + guard case let .peer(peerId, _, _) = self.scope else { return } @@ -2693,13 +3070,206 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.parentController?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } + private func update(transition: ContainedViewLayoutTransition) { + if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: transition) + } + } + + private func gridScrollingOffsetUpdated(transition: ContainedViewLayoutTransition) { + if let _ = self.mapNode, let currentParams = self.currentParams { + self.updateMapLayout(size: currentParams.size, topInset: currentParams.topInset, bottomInset: currentParams.bottomInset, deviceMetrics: currentParams.deviceMetrics, transition: transition) + } + } + + private var effectiveMapHeight: CGFloat = 0.0 + private func updateMapLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) { + guard let mapNode = self.mapNode else { + return + } + + var mapHeight = min(size.width, size.height) + mapHeight = min(mapHeight, floor(size.height * 0.389)) + + let mapOverscrollInset: CGFloat = size.height - mapHeight + + self.effectiveMapHeight = mapHeight - self.additionalNavigationHeight + let mapSize = CGSize(width: size.width, height: mapHeight + mapOverscrollInset) + + var controlsTopPadding = mapOverscrollInset + self.additionalNavigationHeight + + let effectiveScrollingOffset: CGFloat + if let items = self.items, items.items.isEmpty, items.count == 0 { + effectiveScrollingOffset = -size.height * 0.5 + 60.0 + bottomInset + } else { + effectiveScrollingOffset = self.itemGrid.scrollingOffset + } + controlsTopPadding += min(0.0, effectiveScrollingOffset) + + let mapFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - mapOverscrollInset - effectiveScrollingOffset - self.additionalNavigationHeight), size: mapSize) + transition.updateFrame(node: mapNode, frame: mapFrame) + + let mapOffset = min(floorToScreenPixels(effectiveScrollingOffset * 0.5), mapSize.height) + + mapNode.updateLayout( + layout: ContainerViewLayout( + size: mapSize, + metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), + deviceMetrics: deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: mapOverscrollInset, left: 0.0, bottom: 0.0, right: 0.0), + safeInsets: UIEdgeInsets(), + additionalInsets: UIEdgeInsets(), + statusBarHeight: nil, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ), + navigationBarHeight: 0.0, + topPadding: mapOverscrollInset + self.additionalNavigationHeight, + controlsTopPadding: controlsTopPadding, + offset: mapOffset, + size: mapSize, + transition: transition + ) + + if let mapInfoData = self.mapInfoData { + let mapInfoNode: LocationInfoListItemNode + if let current = self.mapInfoNode { + mapInfoNode = current + } else { + mapInfoNode = LocationInfoListItemNode() + mapInfoNode.isUserInteractionEnabled = false + self.mapInfoNode = mapInfoNode + mapNode.supernode?.insertSubnode(mapInfoNode, aboveSubnode: mapNode) + mapInfoNode.clipsToBounds = true + mapInfoNode.cornerRadius = 10.0 + mapInfoNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } + mapInfoNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + let addressString: String? + if let address = mapInfoData.address { + addressString = address + } else { + addressString = self.presentationData.strings.Map_Locating + } + let distanceString: String? + if let distance = mapInfoData.distance { + distanceString = distance < 10 ? self.presentationData.strings.Map_YouAreHere : self.presentationData.strings.Map_DistanceAway(stringForDistance(strings: self.presentationData.strings, distance: distance)).string + } else { + distanceString = nil + } + + let item = LocationInfoListItem( + presentationData: ItemListPresentationData(self.presentationData), + engine: self.context.engine, + location: mapInfoData.location, + address: addressString, + distance: distanceString, + drivingTime: mapInfoData.drivingTime, + transitTime: mapInfoData.transitTime, + walkingTime: mapInfoData.walkingTime, + hasEta: mapInfoData.hasEta, + action: {}, + drivingAction: {}, + transitAction: {}, + walkingAction: {} + ) + let (mapInfoLayout, mapInfoReadyAndApply) = mapInfoNode.asyncLayout()( + item, + ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 1000.0, isStandalone: true) + ) + + let mapInfoTopInset: CGFloat = -6.0 + + let mapInfoFrame = CGRect(origin: CGPoint(x: 0.0, y: mapFrame.maxY + mapInfoTopInset), size: mapInfoLayout.contentSize) + transition.updateFrame(node: mapInfoNode, frame: mapInfoFrame) + mapInfoReadyAndApply().1(ListViewItemApply(isOnScreen: true)) + + self.effectiveMapHeight += mapInfoLayout.contentSize.height + mapInfoTopInset + + if let itemCount = self.itemCount, itemCount != 0 { + let searchHeader: ComponentView + if let current = self.searchHeader { + searchHeader = current + } else { + searchHeader = ComponentView() + self.searchHeader = searchHeader + } + let searchHeaderSize = searchHeader.update( + transition: ComponentTransition(transition), + component: AnyComponent(StorySearchHeaderComponent( + theme: self.presentationData.theme, + strings: self.presentationData.strings, + count: itemCount + )), + environment: {}, + containerSize: CGSize(width: size.width, height: 1000.0) + ) + let searchHeaderFrame = CGRect(origin: CGPoint(x: 0.0, y: max(topInset, mapInfoFrame.maxY)), size: searchHeaderSize) + if let searchHeaderView = searchHeader.view { + if searchHeaderView.superview == nil { + self.view.addSubview(searchHeaderView) + } + transition.updateFrame(view: searchHeaderView, frame: searchHeaderFrame) + } + self.effectiveMapHeight += searchHeaderSize.height + } else { + if let searchHeader = self.searchHeader { + self.searchHeader = nil + searchHeader.view?.removeFromSuperview() + } + } + } else { + if let mapInfoNode = self.mapInfoNode { + self.mapInfoNode = nil + mapInfoNode.removeFromSupernode() + } + if let searchHeader = self.searchHeader { + self.searchHeader = nil + searchHeader.view?.removeFromSuperview() + } + } + } + public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { self.currentParams = (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) + var gridTopInset = topInset + + if self.mapNode != nil { + self.updateMapLayout(size: size, topInset: topInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, transition: transition) + gridTopInset += self.effectiveMapHeight + + let mapOptionsNode: LocationOptionsNode + if let current = self.mapOptionsNode { + mapOptionsNode = current + } else { + mapOptionsNode = LocationOptionsNode(presentationData: self.presentationData, hasBackground: false, updateMapMode: { [weak self] mode in + guard let self else { + return + } + + var state = self.locationViewState + state.mapMode = mode + state.displayingMapModeOptions = false + self.locationViewState = state + }) + mapOptionsNode.clipsToBounds = true + self.mapOptionsNode = mapOptionsNode + self.parentController?.navigationBar?.additionalContentNode.addSubnode(mapOptionsNode) + } + + let mapOptionsFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - self.additionalNavigationHeight), size: CGSize(width: size.width, height: self.additionalNavigationHeight)) + transition.updatePosition(node: mapOptionsNode, position: mapOptionsFrame.center) + transition.updateBounds(node: mapOptionsNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: 38.0 - self.additionalNavigationHeight), size: mapOptionsFrame.size)) + mapOptionsNode.updateLayout(size: mapOptionsFrame.size, leftInset: sideInset, rightInset: sideInset, transition: transition) + } + var bottomInset = bottomInset - if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, let peerId = self.peerId { + if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, case let .peer(peerId, _, isArchived) = self.scope { let selectionPanel: ComponentView - var selectionPanelTransition = Transition(transition) + var selectionPanelTransition = ComponentTransition(transition) if let current = self.selectionPanel { selectionPanel = current } else { @@ -2767,7 +3337,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr selectionItems.append(BottomActionsPanelComponent.Item( id: "archive", color: .accent, - title: self.isArchive ? presentationData.strings.StoryList_ActionPanel_Unarchive : presentationData.strings.StoryList_ActionPanel_Archive, + title: isArchived ? presentationData.strings.StoryList_ActionPanel_Unarchive : presentationData.strings.StoryList_ActionPanel_Archive, isEnabled: !selectedIds.isEmpty, action: { [weak self] in guard let self, let _ = self.itemInteraction.selectedIds else { @@ -2780,10 +3350,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr parentController.cancelItemSelection() } - let _ = self.context.engine.messages.updateStoriesArePinned(peerId: peerId, ids: items, isPinned: self.isArchive ? true : false).startStandalone() + let _ = self.context.engine.messages.updateStoriesArePinned(peerId: peerId, ids: items, isPinned: isArchived ? true : false).startStandalone() let text: String - if self.isArchive { + if isArchived { text = presentationData.strings.StoryList_ToastUnarchived_Text(Int32(items.count)) } else { text = presentationData.strings.StoryList_ToastArchived_Text(Int32(items.count)) @@ -2833,9 +3403,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr transition.updateFrame(node: self.contextGestureContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) - if let items = self.items, items.items.isEmpty, items.count == 0 { + if case let .peer(_, _, isArchived) = self.scope, let items = self.items, items.items.isEmpty, items.count == 0 { let emptyStateView: ComponentView - var emptyStateTransition = Transition(transition) + var emptyStateTransition = ComponentTransition(transition) if let current = self.emptyStateView { emptyStateView = current } else { @@ -2850,16 +3420,16 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr theme: presentationData.theme, fitToHeight: self.isProfileEmbedded, animationName: "StoryListEmpty", - title: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyPosts_Title, - text: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyPosts_Text, - actionTitle: self.isArchive ? nil : presentationData.strings.StoryList_SavedAddAction, + title: isArchived ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyPosts_Title, + text: isArchived ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyPosts_Text, + actionTitle: isArchived ? nil : presentationData.strings.StoryList_SavedAddAction, action: { [weak self] in guard let self else { return } self.emptyAction?() }, - additionalActionTitle: (self.isArchive || self.isProfileEmbedded) ? nil : presentationData.strings.StoryList_SavedEmptyAction, + additionalActionTitle: (isArchived || self.isProfileEmbedded) ? nil : presentationData.strings.StoryList_SavedEmptyAction, additionalAction: { [weak self] in guard let self else { return @@ -2868,14 +3438,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } )), environment: {}, - containerSize: CGSize(width: size.width, height: size.height - topInset - bottomInset) + containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset) ) let emptyStateFrame: CGRect if self.isProfileEmbedded { - emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: max(topInset, floor((visibleHeight - topInset - bottomInset - emptyStateSize.height) * 0.5))), size: emptyStateSize) + emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: max(gridTopInset, floor((visibleHeight - gridTopInset - bottomInset - emptyStateSize.height) * 0.5))), size: emptyStateSize) } else { - emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: topInset), size: emptyStateSize) + emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: gridTopInset), size: emptyStateSize) } if let emptyStateComponentView = emptyStateView.view { @@ -2896,13 +3466,13 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } if self.didUpdateItemsOnce { - Transition(animation: .curve(duration: 0.2, curve: .easeInOut)).setBackgroundColor(view: self.view, color: backgroundColor) + ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)).setBackgroundColor(view: self.view, color: backgroundColor) } else { self.view.backgroundColor = backgroundColor } } else { if let emptyStateView = self.emptyStateView { - let subTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + let subTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) self.emptyStateView = nil if let emptyStateComponentView = emptyStateView.view { @@ -2927,14 +3497,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.didUpdateItemsOnce = true let fixedItemHeight: CGFloat? let isList = false - switch self.contentType { - default: - fixedItemHeight = nil - } + fixedItemHeight = nil let fixedItemAspect: CGFloat? = 0.81 - - let gridTopInset = topInset self.itemGrid.pinchEnabled = items.count > 2 self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none) @@ -3261,7 +3826,7 @@ private final class BottomActionsPanelComponent: Component { } - func update(component: BottomActionsPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BottomActionsPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme self.component = component @@ -3366,7 +3931,7 @@ private final class BottomActionsPanelComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index a9be6031449..8d31667d61b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -1014,6 +1014,9 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme func coveringInsetOffsetUpdated(transition: ContainedViewLayoutTransition) { self.coveringInsetOffsetUpdatedImpl?(transition) } + + func scrollingOffsetUpdated(transition: ContainedViewLayoutTransition) { + } func onBeginFastScrolling() { self.onBeginFastScrollingImpl?() @@ -1192,7 +1195,7 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, chatControllerInteraction.openInstantPage(message, data) }, longTap: { action, message in - chatControllerInteraction.longTap(action, message) + chatControllerInteraction.longTap(action, ChatControllerInteraction.LongTapParams(message: message)) }, getHiddenMedia: { return chatControllerInteraction.hiddenMedia diff --git a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/BUILD b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/BUILD new file mode 100644 index 00000000000..b4afc963ea8 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/BUILD @@ -0,0 +1,34 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "OldChannelsController", + module_name = "OldChannelsController", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/TelegramPresentationData", + "//submodules/PresentationDataUtils", + "//submodules/AccountContext", + "//submodules/ContactsPeerItem", + "//submodules/ItemListUI", + "//submodules/SearchUI", + "//submodules/SearchBarNode", + "//submodules/SolidRoundedButtonNode", + "//submodules/PremiumUI", + "//submodules/ChatListSearchItemHeader", + "//submodules/MergeLists", + ], + visibility = [ + "//visibility:public", + ], +) + + diff --git a/submodules/PeerInfoUI/Sources/OldChannelsController.swift b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift similarity index 99% rename from submodules/PeerInfoUI/Sources/OldChannelsController.swift rename to submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift index 84370d42947..85d0544966d 100644 --- a/submodules/PeerInfoUI/Sources/OldChannelsController.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift @@ -191,7 +191,7 @@ private struct OldChannelsState: Equatable { private func oldChannelsEntries(presentationData: PresentationData, state: OldChannelsState, isPremium: Bool, isPremiumDisabled: Bool, limit: Int32, premiumLimit: Int32, peers: [InactiveChannel]?, intent: OldChannelsControllerIntent) -> [OldChannelsEntry] { var entries: [OldChannelsEntry] = [] - let count = max(limit, Int32(peers?.count ?? 0)) + let count = max(isPremium ? premiumLimit : limit, Int32(peers?.count ?? 0)) var text: String? if count >= premiumLimit { switch intent { @@ -363,7 +363,7 @@ public func oldChannelsController(context: AccountContext, updatedPresentationDa } let footerItem: IncreaseLimitFooterItem? - if (state.isSearching || premiumConfiguration.isPremiumDisabled) && state.selectedPeers.count == 0 { + if (state.isSearching || premiumConfiguration.isPremiumDisabled || isPremium) && state.selectedPeers.count == 0 { footerItem = nil } else { footerItem = IncreaseLimitFooterItem(theme: presentationData.theme, title: buttonText, colorful: colorful, action: { diff --git a/submodules/PeerInfoUI/Sources/OldChannelsSearch.swift b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsSearch.swift similarity index 98% rename from submodules/PeerInfoUI/Sources/OldChannelsSearch.swift rename to submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsSearch.swift index cb679ae79ec..1575419d2e4 100644 --- a/submodules/PeerInfoUI/Sources/OldChannelsSearch.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsSearch.swift @@ -14,6 +14,19 @@ import SearchUI import ChatListSearchItemHeader import ContactsPeerItem +//Xcode 16 +#if canImport(ContactProvider) +extension NavigationBarSearchContentNode: @retroactive ItemListControllerSearchNavigationContentNode { + public func activate() { + } + + public func deactivate() { + } + + public func setQueryUpdated(_ f: @escaping (String) -> Void) { + } +} +#else extension NavigationBarSearchContentNode: ItemListControllerSearchNavigationContentNode { public func activate() { } @@ -24,6 +37,7 @@ extension NavigationBarSearchContentNode: ItemListControllerSearchNavigationCont public func setQueryUpdated(_ f: @escaping (String) -> Void) { } } +#endif final class OldChannelsSearchItem: ItemListControllerSearch { let context: AccountContext diff --git a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD new file mode 100644 index 00000000000..86c4687698e --- /dev/null +++ b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD @@ -0,0 +1,30 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "OwnershipTransferController", + module_name = "OwnershipTransferController", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/TelegramPresentationData", + "//submodules/PresentationDataUtils", + "//submodules/AccountContext", + "//submodules/TextFormat", + "//submodules/AlertUI", + "//submodules/PasswordSetupUI", + "//submodules/Markdown", + "//submodules/ActivityIndicator", + "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/PeerInfoUI/Sources/ChannelOwnershipTransferController.swift b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/ChannelOwnershipTransferController.swift similarity index 98% rename from submodules/PeerInfoUI/Sources/ChannelOwnershipTransferController.swift rename to submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/ChannelOwnershipTransferController.swift index 9a939217bc3..e68b26f8273 100644 --- a/submodules/PeerInfoUI/Sources/ChannelOwnershipTransferController.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/ChannelOwnershipTransferController.swift @@ -12,6 +12,7 @@ import AlertUI import PresentationDataUtils import PasswordSetupUI import Markdown +import OldChannelsController private final class ChannelOwnershipTransferPasswordFieldNode: ASDisplayNode, UITextFieldDelegate { private var theme: PresentationTheme @@ -562,7 +563,7 @@ private func confirmChannelOwnershipTransferController(context: AccountContext, return controller } -func channelOwnershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer, member: TelegramUser, initialError: ChannelOwnershipTransferError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (EnginePeer.Id?) -> Void) -> ViewController { +public func channelOwnershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer, member: TelegramUser, initialError: ChannelOwnershipTransferError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (EnginePeer.Id?) -> Void) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } let theme = AlertControllerTheme(presentationData: presentationData) diff --git a/submodules/TelegramUI/Sources/OwnershipTransferController.swift b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/OwnershipTransferController.swift similarity index 93% rename from submodules/TelegramUI/Sources/OwnershipTransferController.swift rename to submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/OwnershipTransferController.swift index f721758b0ea..4d5f6e9edc6 100644 --- a/submodules/TelegramUI/Sources/OwnershipTransferController.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/OwnershipTransferController.swift @@ -12,7 +12,6 @@ import AlertUI import PresentationDataUtils import PasswordSetupUI import Markdown -import PeerInfoUI private func commitOwnershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, present: @escaping (ViewController, Any?) -> Void, commit: @escaping (String) -> Signal, completion: @escaping (MessageActionCallbackResult) -> Void) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } @@ -79,7 +78,7 @@ private func commitOwnershipTransferController(context: AccountContext, updatedP } -func ownershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, initialError: MessageActionCallbackError, present: @escaping (ViewController, Any?) -> Void, commit: @escaping (String) -> Signal, completion: @escaping (MessageActionCallbackResult) -> Void) -> ViewController { +public func ownershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, initialError: MessageActionCallbackError, present: @escaping (ViewController, Any?) -> Void, commit: @escaping (String) -> Signal, completion: @escaping (MessageActionCallbackResult) -> Void) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } let theme = AlertControllerTheme(presentationData: presentationData) diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 8b26941aac8..c2986af79e3 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -377,7 +377,8 @@ final class PeerSelectionControllerNode: ASDisplayNode { chatLocation: .peer(id: strongSelf.context.account.peerId), subject: .messageOptions(peerIds: peerIds, ids: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .forward(ChatControllerSubject.MessageOptionsInfo.Forward(options: forwardOptions))), botStart: nil, - mode: .standard(.previewing) + mode: .standard(.previewing), + params: nil ) chatController.canReadHistory.set(false) @@ -680,58 +681,67 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, reportPeerIrrelevantGeoLocation: { }, displaySlowmodeTooltip: { _, _ in }, displaySendMessageOptions: { [weak self] node, gesture in - guard let strongSelf = self, let textInputPanelNode = strongSelf.textInputPanelNode else { - return - } - textInputPanelNode.loadTextInputNodeIfNeeded() - guard let textInputNode = textInputPanelNode.textInputNode else { + guard let strongSelf = self else { return } - var hasEntityKeyboard = false - if case .media = strongSelf.presentationInterfaceState.inputMode { - hasEntityKeyboard = true - } - - let controller = makeChatSendMessageActionSheetController( - context: strongSelf.context, - peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, - params: .sendMessage(SendMessageActionSheetControllerParams.SendMessage( - isScheduledMessages: false, - mediaPreview: nil, - mediaCaptionIsAbove: nil, - attachment: false, - canSendWhenOnline: false, - forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] - )), - hasEntityKeyboard: hasEntityKeyboard, - gesture: gesture, - sourceSendButton: node, - textInputView: textInputNode.textView, - emojiViewProvider: textInputPanelNode.emojiViewProvider, - completion: { - }, - sendMessage: { [weak textInputPanelNode] mode, messageEffect in - switch mode { - case .generic: - textInputPanelNode?.sendMessage(.generic, messageEffect) - case .silently: - textInputPanelNode?.sendMessage(.silent, messageEffect) - case .whenOnline: - textInputPanelNode?.sendMessage(.whenOnline, messageEffect) - } - }, - schedule: { [weak textInputPanelNode] messageEffect in - textInputPanelNode?.sendMessage(.schedule, messageEffect) - }, - openPremiumPaywall: { [weak controller] c in - guard let controller else { - return - } - controller.push(c) + let _ = (ChatSendMessageContextScreen.initialData(context: strongSelf.context, currentMessageEffectId: nil) + |> deliverOnMainQueue).start(next: { initialData in + guard let strongSelf = self, let textInputPanelNode = strongSelf.textInputPanelNode else { + return } - ) - strongSelf.presentInGlobalOverlay(controller, nil) + textInputPanelNode.loadTextInputNodeIfNeeded() + guard let textInputNode = textInputPanelNode.textInputNode else { + return + } + + var hasEntityKeyboard = false + if case .media = strongSelf.presentationInterfaceState.inputMode { + hasEntityKeyboard = true + } + + let controller = makeChatSendMessageActionSheetController( + initialData: initialData, + context: strongSelf.context, + peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, + params: .sendMessage(SendMessageActionSheetControllerParams.SendMessage( + isScheduledMessages: false, + mediaPreview: nil, + mediaCaptionIsAbove: nil, + messageEffect: nil, + attachment: false, + canSendWhenOnline: false, + forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] + )), + hasEntityKeyboard: hasEntityKeyboard, + gesture: gesture, + sourceSendButton: node, + textInputView: textInputNode.textView, + emojiViewProvider: textInputPanelNode.emojiViewProvider, + completion: { + }, + sendMessage: { [weak textInputPanelNode] mode, messageEffect in + switch mode { + case .generic: + textInputPanelNode?.sendMessage(.generic, messageEffect) + case .silently: + textInputPanelNode?.sendMessage(.silent, messageEffect) + case .whenOnline: + textInputPanelNode?.sendMessage(.whenOnline, messageEffect) + } + }, + schedule: { [weak textInputPanelNode] messageEffect in + textInputPanelNode?.sendMessage(.schedule, messageEffect) + }, + openPremiumPaywall: { [weak controller] c in + guard let controller else { + return + } + controller.push(c) + } + ) + strongSelf.presentInGlobalOverlay(controller, nil) + }) }, openScheduledMessages: { }, openPeersNearby: { }, displaySearchResultsTooltip: { _, _ in diff --git a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift index 74a091362b8..8e941d1105d 100644 --- a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift +++ b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift @@ -130,7 +130,7 @@ public final class PlainButtonComponent: Component { self.contentContainer.alpha = 0.7 } if animateScale { - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) transition.setScale(layer: self.contentContainer.layer, scale: topScale) } } else { @@ -140,7 +140,7 @@ public final class PlainButtonComponent: Component { } if animateScale { - let transition = Transition(animation: .none) + let transition = ComponentTransition(animation: .none) transition.setScale(layer: self.contentContainer.layer, scale: 1.0) self.contentContainer.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in @@ -188,7 +188,7 @@ public final class PlainButtonComponent: Component { return nil } - func update(component: PlainButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PlainButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.componentState = state @@ -294,7 +294,7 @@ public final class PlainButtonComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift index 42472817e76..094d6f7c8ff 100644 --- a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift @@ -307,7 +307,7 @@ public final class GiftAvatarComponent: Component { } } - func update(component: GiftAvatarComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: GiftAvatarComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component self.setup() @@ -384,6 +384,10 @@ public final class GiftAvatarComponent: Component { iconBackgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24)) iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment") iconOffset = 5.0 + case .ads: + iconBackgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24)) + iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment") + iconOffset = 5.0 case .premiumBot: iconInset = 15.0 iconBackgroundView.image = generateGradientFilledCircleImage( @@ -492,7 +496,7 @@ public final class GiftAvatarComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/PremiumStarComponent.swift b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/PremiumStarComponent.swift index 5460e41b6cc..495541776bc 100644 --- a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/PremiumStarComponent.swift +++ b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/PremiumStarComponent.swift @@ -656,7 +656,7 @@ public final class PremiumStarComponent: Component { node.addAnimation(springAnimation, forKey: "rotate") } - func update(component: PremiumStarComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: PremiumStarComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component self.setup() @@ -680,7 +680,7 @@ public final class PremiumStarComponent: Component { return View(frame: CGRect(), isIntro: self.isIntro) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/Sources/PremiumPeerShortcutComponent.swift b/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/Sources/PremiumPeerShortcutComponent.swift index f65f1d30606..65694c3c57f 100644 --- a/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/Sources/PremiumPeerShortcutComponent.swift +++ b/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/Sources/PremiumPeerShortcutComponent.swift @@ -56,7 +56,7 @@ public final class PremiumPeerShortcutComponent: Component { fatalError("init(coder:) has not been implemented") } - public func update(component: PremiumPeerShortcutComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(component: PremiumPeerShortcutComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -100,7 +100,7 @@ public final class PremiumPeerShortcutComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen/Sources/SavedMessagesScreen.swift b/submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen/Sources/SavedMessagesScreen.swift index 9fd1b3bdaba..45de9284bf5 100644 --- a/submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen/Sources/SavedMessagesScreen.swift +++ b/submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen/Sources/SavedMessagesScreen.swift @@ -42,7 +42,7 @@ private final class SavedMessagesScreenComponent: Component { deinit { } - func update(component: SavedMessagesScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: SavedMessagesScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value let themeUpdated = self.environment?.theme !== environment.theme @@ -63,7 +63,7 @@ private final class SavedMessagesScreenComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ScrollComponent/Sources/ScrollComponent.swift b/submodules/TelegramUI/Components/ScrollComponent/Sources/ScrollComponent.swift index 9f4cb768d6e..a868bbb53c4 100644 --- a/submodules/TelegramUI/Components/ScrollComponent/Sources/ScrollComponent.swift +++ b/submodules/TelegramUI/Components/ScrollComponent/Sources/ScrollComponent.swift @@ -108,7 +108,7 @@ public final class ScrollComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ScrollComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ScrollComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let contentSize = self.contentView.update( transition: transition, component: component.content, @@ -144,7 +144,7 @@ public final class ScrollComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/PeerListItemComponent.swift index 55e3f10ea64..bf0e0b0cd13 100644 --- a/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/PeerListItemComponent.swift @@ -153,7 +153,7 @@ final class PeerListItemComponent: Component { component.action(peer) } - func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme var hasSelectionUpdated = false @@ -367,7 +367,7 @@ final class PeerListItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift index b75ef97dde0..9cbdf82a8a6 100644 --- a/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift +++ b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift @@ -222,7 +222,7 @@ private final class SendInviteLinkScreenComponent: Component { } } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { return } @@ -266,7 +266,7 @@ private final class SendInviteLinkScreenComponent: Component { } } - func update(component: SendInviteLinkScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: SendInviteLinkScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value let themeUpdated = self.environment?.theme !== environment.theme @@ -785,7 +785,7 @@ private final class SendInviteLinkScreenComponent: Component { } else { self.selectedItems.insert(peer.id) } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) } )), environment: {}, @@ -942,7 +942,7 @@ private final class SendInviteLinkScreenComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift index 023da0812a4..bcc480988f6 100644 --- a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift +++ b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift @@ -100,7 +100,7 @@ public final class ArchiveInfoContentComponent: Component { } } - func update(component: ArchiveInfoContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ArchiveInfoContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let sideInset: CGFloat = 16.0 @@ -326,7 +326,7 @@ public final class ArchiveInfoContentComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift index 59df7851f94..0bee05ace12 100644 --- a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift @@ -46,7 +46,7 @@ private final class ArchiveInfoSheetContentComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ArchiveInfoSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ArchiveInfoSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let environment = environment[EnvironmentType.self].value @@ -122,7 +122,7 @@ private final class ArchiveInfoSheetContentComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -169,7 +169,7 @@ private final class ArchiveInfoScreenComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ArchiveInfoScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ArchiveInfoScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let environment = environment[ViewControllerComponentContainer.Environment.self].value @@ -250,7 +250,7 @@ private final class ArchiveInfoScreenComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift index 97ce5455f1f..13114175dbb 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift @@ -96,7 +96,7 @@ final class GreetingMessageListItemComponent: Component { self.component?.action?() } - func update(component: GreetingMessageListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: GreetingMessageListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component self.componentState = state @@ -318,7 +318,7 @@ final class GreetingMessageListItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift index 44d712798c4..289234bb625 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift @@ -302,7 +302,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { } var scrolledUp = true - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { let navigationRevealOffsetY: CGFloat = 0.0 let navigationAlphaDistance: CGFloat = 16.0 @@ -483,7 +483,8 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { chatLocation: .customChatContents, subject: .customChatContents(contents: contents), botStart: nil, - mode: .standard(.default) + mode: .standard(.default), + params: nil ) chatController.navigationPresentation = .modal self.environment?.controller()?.push(chatController) @@ -574,7 +575,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { } } - func update(component: AutomaticBusinessMessageSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: AutomaticBusinessMessageSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -675,7 +676,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { self.component = component self.state = state - let alphaTransition: Transition = transition.animation.isImmediate ? transition : transition.withAnimation(.curve(duration: 0.25, curve: .easeInOut)) + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? transition : transition.withAnimation(.curve(duration: 0.25, curve: .easeInOut)) if themeUpdated { self.backgroundColor = environment.theme.list.blocksBackgroundColor @@ -1536,7 +1537,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BottomPanelComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BottomPanelComponent.swift index 2dd0f3bae71..75f84dc8b12 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BottomPanelComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BottomPanelComponent.swift @@ -55,7 +55,7 @@ final class BottomPanelComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: BottomPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BottomPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component self.componentState = state @@ -111,7 +111,7 @@ final class BottomPanelComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift index 892f133f750..16d7b4488ff 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift @@ -136,11 +136,11 @@ final class BusinessLinkListItemComponent: Component { } self.isExtractedToContextMenu = value - let mappedTransition: Transition + let mappedTransition: ComponentTransition if value { - mappedTransition = Transition(transition) + mappedTransition = ComponentTransition(transition) } else { - mappedTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + mappedTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) } self.componentState?.updated(transition: mappedTransition) } @@ -162,7 +162,7 @@ final class BusinessLinkListItemComponent: Component { self.component?.action() } - func update(component: BusinessLinkListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BusinessLinkListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component let _ = previousComponent @@ -334,7 +334,7 @@ final class BusinessLinkListItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift index 86ea07efa8e..6fee2e2d3e5 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift @@ -115,7 +115,7 @@ final class BusinessLinksSetupScreenComponent: Component { } var scrolledUp = true - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { let navigationRevealOffsetY: CGFloat = 0.0 let navigationAlphaDistance: CGFloat = 16.0 @@ -212,7 +212,8 @@ final class BusinessLinksSetupScreenComponent: Component { chatLocation: .customChatContents, subject: .customChatContents(contents: contents), botStart: nil, - mode: .standard(.default) + mode: .standard(.default), + params: nil ) if openKeyboard { chatController.activateInput(type: .text) @@ -259,7 +260,7 @@ final class BusinessLinksSetupScreenComponent: Component { environment.controller()?.present(ShareController(context: component.context, subject: .url(link.url), showInChat: nil, externalShare: false, immediateExternalShare: false), in: .window(.root)) } - func update(component: BusinessLinksSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BusinessLinksSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -293,7 +294,7 @@ final class BusinessLinksSetupScreenComponent: Component { self.component = component self.state = state - let alphaTransition: Transition + let alphaTransition: ComponentTransition if !transition.animation.isImmediate { alphaTransition = .easeInOut(duration: 0.25) } else { @@ -653,7 +654,7 @@ final class BusinessLinksSetupScreenComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift index 4582101ed6f..cfd8eb14a21 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift @@ -57,7 +57,7 @@ final class QuickReplyEmptyStateComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: QuickReplyEmptyStateComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: QuickReplyEmptyStateComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component self.componentState = state @@ -180,7 +180,7 @@ final class QuickReplyEmptyStateComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index f2ebe3a3053..9409f8a6a11 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -389,7 +389,7 @@ final class QuickReplySetupScreenComponent: Component { } } - func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) { + func update(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) { let (listViewDuration, listViewCurve) = listViewAnimationDurationAndCurve(transition: transition.containedViewLayoutTransition) self.transaction( deleteIndices: [], @@ -578,7 +578,8 @@ final class QuickReplySetupScreenComponent: Component { chatLocation: .customChatContents, subject: .customChatContents(contents: contents), botStart: nil, - mode: .standard(.default) + mode: .standard(.default), + params: nil ) chatController.navigationPresentation = .modal @@ -723,7 +724,7 @@ final class QuickReplySetupScreenComponent: Component { insets: UIEdgeInsets, statusBarHeight: CGFloat, isModal: Bool, - transition: Transition, + transition: ComponentTransition, deferScrollApplication: Bool ) -> CGFloat { var rightButtons: [AnyComponentWithIdentity] = [] @@ -849,7 +850,7 @@ final class QuickReplySetupScreenComponent: Component { } } - private func updateNavigationScrolling(navigationHeight: CGFloat, transition: Transition) { + private func updateNavigationScrolling(navigationHeight: CGFloat, transition: ComponentTransition) { var mainOffset: CGFloat if let shortcutMessageList = self.shortcutMessageList, !shortcutMessageList.items.isEmpty { if let contentListNode = self.contentListNode { @@ -888,7 +889,7 @@ final class QuickReplySetupScreenComponent: Component { } } - func update(component: QuickReplySetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: QuickReplySetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -919,7 +920,7 @@ final class QuickReplySetupScreenComponent: Component { self.component = component self.state = state - let alphaTransition: Transition = transition.animation.isImmediate ? transition : transition.withAnimation(.curve(duration: 0.25, curve: .easeInOut)) + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? transition : transition.withAnimation(.curve(duration: 0.25, curve: .easeInOut)) let _ = alphaTransition if themeUpdated { @@ -1271,7 +1272,7 @@ final class QuickReplySetupScreenComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerComponent.swift b/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerComponent.swift index 527dbd8f577..44a581ee4fa 100644 --- a/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerComponent.swift +++ b/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerComponent.swift @@ -79,7 +79,7 @@ public final class BirthdayPickerComponent: Component { preconditionFailure() } - func update(component: BirthdayPickerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BirthdayPickerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let isFirstTime = self.component == nil self.component = component self.componentState = state @@ -239,7 +239,7 @@ public final class BirthdayPickerComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerContentComponent.swift b/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerContentComponent.swift index d4e311d8a05..16ed898dd4c 100644 --- a/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerContentComponent.swift +++ b/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerContentComponent.swift @@ -83,7 +83,7 @@ public final class BirthdayPickerContentComponent: Component { } } - func update(component: BirthdayPickerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BirthdayPickerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -226,7 +226,7 @@ public final class BirthdayPickerContentComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerScreen.swift b/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerScreen.swift index cc7d1af9460..f3de37cbbe8 100644 --- a/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerScreen.swift @@ -53,7 +53,7 @@ private final class BirthdayPickerSheetContentComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: BirthdayPickerSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BirthdayPickerSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let environment = environment[EnvironmentType.self].value @@ -157,7 +157,7 @@ private final class BirthdayPickerSheetContentComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -205,7 +205,7 @@ private final class BirthdayPickerScreenComponent: Component { } private var didAppear = false - func update(component: BirthdayPickerScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BirthdayPickerScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let environment = environment[ViewControllerComponentContainer.Environment.self].value @@ -267,6 +267,7 @@ private final class BirthdayPickerScreenComponent: Component { } )), backgroundColor: .color(environment.theme.list.plainBackgroundColor), + isScrollEnabled: false, animateOut: self.sheetAnimateOut )), environment: { @@ -290,7 +291,7 @@ private final class BirthdayPickerScreenComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/BoostLevelIconComponent/Sources/BoostLevelIconComponent.swift b/submodules/TelegramUI/Components/Settings/BoostLevelIconComponent/Sources/BoostLevelIconComponent.swift index 5f8d9d42d95..81ec76f9443 100644 --- a/submodules/TelegramUI/Components/Settings/BoostLevelIconComponent/Sources/BoostLevelIconComponent.swift +++ b/submodules/TelegramUI/Components/Settings/BoostLevelIconComponent/Sources/BoostLevelIconComponent.swift @@ -80,7 +80,7 @@ public final class BoostLevelIconComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: BoostLevelIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BoostLevelIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.component != component { self.imageView.image = generateDisclosureActionBoostLevelBadgeImage(text: component.strings.Channel_Appearance_BoostLevel("\(component.level)").string) } @@ -99,7 +99,7 @@ public final class BoostLevelIconComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift index 35689013c9f..66fbf35d6bf 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift @@ -144,7 +144,7 @@ final class BusinessDaySetupScreenComponent: Component { } var scrolledUp = true - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { let navigationRevealOffsetY: CGFloat = 0.0 let navigationAlphaDistance: CGFloat = 16.0 @@ -233,7 +233,7 @@ final class BusinessDaySetupScreenComponent: Component { } } - func update(component: BusinessDaySetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BusinessDaySetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -483,7 +483,7 @@ final class BusinessDaySetupScreenComponent: Component { if let rangeSectionView = rangeSection.view { if !transition.animation.isImmediate { - Transition.easeInOut(duration: 0.2).setAlpha(view: rangeSectionView, alpha: 0.0, completion: { [weak rangeSectionView] _ in + ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: rangeSectionView, alpha: 0.0, completion: { [weak rangeSectionView] _ in rangeSectionView?.removeFromSuperview() }) transition.setScale(view: rangeSectionView, scale: 0.001) @@ -616,7 +616,7 @@ final class BusinessDaySetupScreenComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift index 7166538490d..f1b2d19657f 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift @@ -339,7 +339,7 @@ final class BusinessHoursSetupScreenComponent: Component { } var scrolledUp = true - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { let navigationRevealOffsetY: CGFloat = 0.0 let navigationAlphaDistance: CGFloat = 16.0 @@ -368,7 +368,7 @@ final class BusinessHoursSetupScreenComponent: Component { } } - func update(component: BusinessHoursSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BusinessHoursSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -817,7 +817,7 @@ final class BusinessHoursSetupScreenComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift index 439834ef2f4..679b12f0048 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift @@ -219,7 +219,7 @@ final class BusinessIntroSetupScreenComponent: Component { } private var scrolledUp = true - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { let navigationRevealOffsetY: CGFloat = 0.0 let navigationAlphaDistance: CGFloat = 16.0 @@ -255,7 +255,7 @@ final class BusinessIntroSetupScreenComponent: Component { } } - func update(component: BusinessIntroSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BusinessIntroSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -690,7 +690,7 @@ final class BusinessIntroSetupScreenComponent: Component { self.component = component self.state = state - let alphaTransition: Transition + let alphaTransition: ComponentTransition if !transition.animation.isImmediate { alphaTransition = .easeInOut(duration: 0.25) } else { @@ -1156,7 +1156,7 @@ final class BusinessIntroSetupScreenComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift index b6b2de5752f..f84b0ddd686 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift @@ -77,7 +77,7 @@ final class ChatIntroItemComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ChatIntroItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChatIntroItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.componentState = state @@ -156,7 +156,7 @@ final class ChatIntroItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift index 752d571ea50..4d9fe5574c4 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift @@ -147,7 +147,7 @@ final class BusinessLocationSetupScreenComponent: Component { } var scrolledUp = true - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { let navigationRevealOffsetY: CGFloat = 0.0 let navigationAlphaDistance: CGFloat = 16.0 @@ -263,7 +263,7 @@ final class BusinessLocationSetupScreenComponent: Component { environment.controller()?.dismiss() } - func update(component: BusinessLocationSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BusinessLocationSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -286,7 +286,7 @@ final class BusinessLocationSetupScreenComponent: Component { self.component = component self.state = state - let alphaTransition: Transition + let alphaTransition: ComponentTransition if !transition.animation.isImmediate { alphaTransition = .easeInOut(duration: 0.25) } else { @@ -623,7 +623,7 @@ final class BusinessLocationSetupScreenComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/MapPreviewComponent.swift b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/MapPreviewComponent.swift index 9e887c62796..b48375c117f 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/MapPreviewComponent.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/MapPreviewComponent.swift @@ -84,7 +84,7 @@ final class MapPreviewComponent: Component { self.component?.action?() } - func update(component: MapPreviewComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: MapPreviewComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component self.componentState = state @@ -133,7 +133,7 @@ final class MapPreviewComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift index 7f82689d756..4ae6b6e02a2 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift @@ -141,7 +141,7 @@ final class BusinessRecipientListScreenComponent: Component { } var scrolledUp = true - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { let navigationRevealOffsetY: CGFloat = 0.0 let navigationAlphaDistance: CGFloat = 16.0 @@ -348,7 +348,7 @@ final class BusinessRecipientListScreenComponent: Component { environment.controller()?.push(controller) } - func update(component: BusinessRecipientListScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: BusinessRecipientListScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -646,7 +646,7 @@ final class BusinessRecipientListScreenComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift index c9b085c8ed0..7ac100863b2 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift @@ -84,7 +84,7 @@ final class ChatbotSearchResultItemComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ChatbotSearchResultItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChatbotSearchResultItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -136,7 +136,7 @@ final class ChatbotSearchResultItemComponent: Component { if let addButtonView = addButton.view { if !transition.animation.isImmediate { transition.setScale(view: addButtonView, scale: 0.001) - Transition.easeInOut(duration: 0.2).setAlpha(view: addButtonView, alpha: 0.0, completion: { [weak addButtonView] _ in + ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: addButtonView, alpha: 0.0, completion: { [weak addButtonView] _ in addButtonView?.removeFromSuperview() }) } else { @@ -186,7 +186,7 @@ final class ChatbotSearchResultItemComponent: Component { if let removeButtonView = removeButton.view { if !transition.animation.isImmediate { transition.setScale(view: removeButtonView, scale: 0.001) - Transition.easeInOut(duration: 0.2).setAlpha(view: removeButtonView, alpha: 0.0, completion: { [weak removeButtonView] _ in + ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: removeButtonView, alpha: 0.0, completion: { [weak removeButtonView] _ in removeButtonView?.removeFromSuperview() }) } else { @@ -330,7 +330,7 @@ final class ChatbotSearchResultItemComponent: Component { self.addSubview(addButtonView) if !transition.animation.isImmediate { transition.animateScale(view: addButtonView, from: 0.001, to: 1.0) - Transition.easeInOut(duration: 0.2).animateAlpha(view: addButtonView, from: 0.0, to: 1.0) + ComponentTransition.easeInOut(duration: 0.2).animateAlpha(view: addButtonView, from: 0.0, to: 1.0) } } addButtonTransition.setFrame(view: addButtonView, frame: addButtonFrame) @@ -346,7 +346,7 @@ final class ChatbotSearchResultItemComponent: Component { self.addSubview(removeButtonView) if !transition.animation.isImmediate { transition.animateScale(view: removeButtonView, from: 0.001, to: 1.0) - Transition.easeInOut(duration: 0.2).animateAlpha(view: removeButtonView, from: 0.0, to: 1.0) + ComponentTransition.easeInOut(duration: 0.2).animateAlpha(view: removeButtonView, from: 0.0, to: 1.0) } } removeButtonTransition.setFrame(view: removeButtonView, frame: removeButtonFrame) @@ -396,7 +396,7 @@ final class ChatbotSearchResultItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index 4571ab2149b..20e342ee4d8 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -220,7 +220,7 @@ final class ChatbotSetupScreenComponent: Component { } var scrolledUp = true - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { let navigationRevealOffsetY: CGFloat = 0.0 let navigationAlphaDistance: CGFloat = 16.0 @@ -476,7 +476,7 @@ final class ChatbotSetupScreenComponent: Component { } } - func update(component: ChatbotSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChatbotSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -1076,7 +1076,7 @@ final class ChatbotSetupScreenComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift index acbd3defb8f..de5d8b52b90 100644 --- a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift +++ b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift @@ -66,7 +66,7 @@ private final class PeerBadgeComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: PeerBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PeerBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let height: CGFloat = 32.0 let avatarPadding: CGFloat = 1.0 @@ -127,7 +127,7 @@ private final class PeerBadgeComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -178,7 +178,7 @@ private final class CollectibleItemInfoScreenContentComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: CollectibleItemInfoScreenContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: CollectibleItemInfoScreenContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let environment = environment[EnvironmentType.self].value @@ -562,7 +562,7 @@ private final class CollectibleItemInfoScreenContentComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -603,7 +603,7 @@ private final class CollectibleItemInfoScreenComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: CollectibleItemInfoScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: CollectibleItemInfoScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let environment = environment[ViewControllerComponentContainer.Environment.self].value @@ -672,7 +672,7 @@ private final class CollectibleItemInfoScreenComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreenContentComponent.swift b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreenContentComponent.swift index 288cbffad0b..0264c33d318 100644 --- a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreenContentComponent.swift +++ b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreenContentComponent.swift @@ -100,7 +100,7 @@ import TelegramCore } } - func update(component: CollectibleItemInfoScreenContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: CollectibleItemInfoScreenContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let sideInset: CGFloat = 16.0 @@ -326,7 +326,7 @@ import TelegramCore return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/Sources/NewSessionInfoContentComponent.swift b/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/Sources/NewSessionInfoContentComponent.swift index 4fbe57d7b65..01c73a85a4d 100644 --- a/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/Sources/NewSessionInfoContentComponent.swift +++ b/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/Sources/NewSessionInfoContentComponent.swift @@ -91,7 +91,7 @@ public final class NewSessionInfoContentComponent: Component { } } - func update(component: NewSessionInfoContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: NewSessionInfoContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let sideInset: CGFloat = 16.0 @@ -238,7 +238,7 @@ public final class NewSessionInfoContentComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/Sources/NewSessionInfoScreen.swift b/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/Sources/NewSessionInfoScreen.swift index 6e879550809..82b71044a47 100644 --- a/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/Sources/NewSessionInfoScreen.swift +++ b/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/Sources/NewSessionInfoScreen.swift @@ -51,7 +51,7 @@ private final class NewSessionInfoSheetContentComponent: Component { self.timer?.invalidate() } - func update(component: NewSessionInfoSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: NewSessionInfoSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.timer == nil { self.timer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] _ in guard let self else { @@ -158,7 +158,7 @@ private final class NewSessionInfoSheetContentComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -202,7 +202,7 @@ private final class NewSessionInfoScreenComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: NewSessionInfoScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: NewSessionInfoScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let environment = environment[ViewControllerComponentContainer.Environment.self].value @@ -270,7 +270,7 @@ private final class NewSessionInfoScreenComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift index c7204300d08..2ca28c9923d 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift @@ -320,7 +320,7 @@ final class ChannelAppearanceScreenComponent: Component { } var scrolledUp = true - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { let navigationAlphaDistance: CGFloat = 16.0 let navigationAlpha: CGFloat = max(0.0, min(1.0, self.scrollView.contentOffset.y / navigationAlphaDistance)) if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { @@ -803,7 +803,7 @@ final class ChannelAppearanceScreenComponent: Component { return false } - func update(component: ChannelAppearanceScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChannelAppearanceScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -1862,7 +1862,7 @@ final class ChannelAppearanceScreenComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/EmojiPickerItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/EmojiPickerItem.swift index a795370b00f..ed4f8b15d02 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/EmojiPickerItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/EmojiPickerItem.swift @@ -307,7 +307,7 @@ private final class EmojiSelectionComponent: Component { deinit { } - func update(component: EmojiSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: EmojiSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.backgroundColor = component.backgroundColor let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85) self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate) @@ -388,7 +388,7 @@ private final class EmojiSelectionComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift index 1ce8589d645..346979e7e51 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift @@ -378,7 +378,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { } let iconSize = CGSize(width: 34.0, height: 34.0) let _ = icon.update( - transition: Transition(animation.transition), + transition: ComponentTransition(animation.transition), component: AnyComponent(EmojiStatusComponent( context: item.context, animationCache: item.context.animationCache, diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift index e83928a46d5..c095335e973 100644 --- a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift @@ -156,7 +156,7 @@ final class PeerSelectionScreenComponent: Component { super.init() } - func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) { + func update(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) { let (listViewDuration, listViewCurve) = listViewAnimationDurationAndCurve(transition: transition.containedViewLayoutTransition) self.transaction( deleteIndices: [], @@ -267,7 +267,7 @@ final class PeerSelectionScreenComponent: Component { insets: UIEdgeInsets, statusBarHeight: CGFloat, isModal: Bool, - transition: Transition, + transition: ComponentTransition, deferScrollApplication: Bool ) -> CGFloat { let rightButtons: [AnyComponentWithIdentity] = [] @@ -362,7 +362,7 @@ final class PeerSelectionScreenComponent: Component { } } - private func updateNavigationScrolling(navigationHeight: CGFloat, transition: Transition) { + private func updateNavigationScrolling(navigationHeight: CGFloat, transition: ComponentTransition) { var mainOffset: CGFloat if let contentListNode = self.contentListNode { switch contentListNode.visibleContentOffset() { @@ -401,7 +401,7 @@ final class PeerSelectionScreenComponent: Component { } } - func update(component: PeerSelectionScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PeerSelectionScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -435,7 +435,7 @@ final class PeerSelectionScreenComponent: Component { self.component = component self.state = state - let alphaTransition: Transition = transition.animation.isImmediate ? transition : transition.withAnimation(.curve(duration: 0.25, curve: .easeInOut)) + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? transition : transition.withAnimation(.curve(duration: 0.25, curve: .easeInOut)) let _ = alphaTransition if themeUpdated { @@ -650,7 +650,7 @@ final class PeerSelectionScreenComponent: Component { } else { if let loadingView = self.loadingView { self.loadingView = nil - let removeTransition: Transition = .easeInOut(duration: 0.2) + let removeTransition: ComponentTransition = .easeInOut(duration: 0.2) removeTransition.setAlpha(view: loadingView, alpha: 0.0, completion: { [weak loadingView] _ in loadingView?.removeFromSuperview() }) @@ -683,7 +683,7 @@ final class PeerSelectionScreenComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift index b696a371ab9..644c55a5123 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift @@ -126,7 +126,7 @@ final class CategoryListItemComponent: Component { } } - func update(component: CategoryListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: CategoryListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme var hasSelectionUpdated = false @@ -324,7 +324,7 @@ final class CategoryListItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift index 1559896c2fe..94ffc18a847 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift @@ -318,7 +318,7 @@ final class CountriesMultiselectionScreenComponent: Component { } } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else { return } @@ -443,7 +443,7 @@ final class CountriesMultiselectionScreenComponent: Component { return } let update = { - let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring)) self.state?.updated(transition: transition) if self.searchStateContext != nil { @@ -526,7 +526,7 @@ final class CountriesMultiselectionScreenComponent: Component { self.visibleSectionHeaders.removeValue(forKey: id) } - let fadeTransition = Transition.easeInOut(duration: 0.25) + let fadeTransition = ComponentTransition.easeInOut(duration: 0.25) if let searchStateContext = self.searchStateContext, case let .countriesSearch(query) = searchStateContext.subject, let value = searchStateContext.stateValue, value.sections.isEmpty { let sideInset: CGFloat = 44.0 let emptyAnimationHeight = 148.0 @@ -624,7 +624,7 @@ final class CountriesMultiselectionScreenComponent: Component { } } - func update(component: CountriesMultiselectionScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: CountriesMultiselectionScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { guard !self.isDismissed else { return availableSize } @@ -740,7 +740,7 @@ final class CountriesMultiselectionScreenComponent: Component { if let countryId = tokenId.base as? String { self.selectedCountries.removeAll(where: { $0 == countryId }) } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.35, curve: .spring))) } )), environment: {}, @@ -759,7 +759,7 @@ final class CountriesMultiselectionScreenComponent: Component { } self.searchStateContext = searchStateContext if applyState { - self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(contentReloaded: true))) + self.state?.updated(transition: ComponentTransition(animation: .none).withUserData(AnimationHint(contentReloaded: true))) } }) applyState = true @@ -995,7 +995,7 @@ final class CountriesMultiselectionScreenComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountryListItemComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountryListItemComponent.swift index 66d8648e20c..2bb961b0556 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountryListItemComponent.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountryListItemComponent.swift @@ -95,7 +95,7 @@ final class CountryListItemComponent: Component { component.action() } - func update(component: CountryListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: CountryListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme var hasSelectionUpdated = false @@ -219,7 +219,7 @@ final class CountryListItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/OptionListItemComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/OptionListItemComponent.swift index 77bcb6a0c5d..f5de0074c13 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/OptionListItemComponent.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/OptionListItemComponent.swift @@ -86,7 +86,7 @@ final class OptionListItemComponent: Component { // } } - func update(component: OptionListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: OptionListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme self.component = component @@ -151,7 +151,7 @@ final class OptionListItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/SectionHeaderComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/SectionHeaderComponent.swift index a62d3888240..6d3d8833110 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/SectionHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/SectionHeaderComponent.swift @@ -62,7 +62,7 @@ final class SectionHeaderComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: SectionHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: SectionHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme self.component = component @@ -158,7 +158,7 @@ final class SectionHeaderComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index e1a3c4f2285..130b1564602 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -296,6 +296,7 @@ final class ShareWithPeersScreenComponent: Component { private var visibleItems: [AnyHashable: ComponentView] = [:] private var visibleSectionBackgrounds: [Int: UIView] = [:] private var visibleSectionFooters: [Int: ComponentView] = [:] + private var itemSizes: [AnyHashable: CGSize] = [:] private var ignoreScrolling: Bool = false private var isDismissed: Bool = false @@ -508,7 +509,7 @@ final class ShareWithPeersScreenComponent: Component { } } } else { - let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) self.state?.updated(transition: transition) self.updateModalOverlayTransition(transition: transition) } @@ -672,6 +673,9 @@ final class ShareWithPeersScreenComponent: Component { } var groupPeerIds: [EnginePeer.Id] = [] for peer in peers { + guard !peer.isDeleted else { + continue + } if !existingPeerIds.contains(peer.id) { self.selectedPeers.append(peer.id) existingPeerIds.insert(peer.id) @@ -682,7 +686,7 @@ final class ShareWithPeersScreenComponent: Component { } else { self.selectedPeers = self.selectedPeers.filter { !peerIds.contains($0) } } - let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring)) self.state?.updated(transition: transition) } @@ -804,7 +808,7 @@ final class ShareWithPeersScreenComponent: Component { }) } - private func updateModalOverlayTransition(transition: Transition) { + private func updateModalOverlayTransition(transition: ComponentTransition) { guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout, !self.isDismissed else { return } @@ -837,7 +841,7 @@ final class ShareWithPeersScreenComponent: Component { } } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else { return } @@ -978,7 +982,7 @@ final class ShareWithPeersScreenComponent: Component { if let self { self.selectedPeers = [] self.selectedGroups = [] - let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring)) self.state?.updated(transition: transition) } } @@ -1022,13 +1026,13 @@ final class ShareWithPeersScreenComponent: Component { } } for i in 0 ..< peers.count { + let peer = peers[i] + let itemId = AnyHashable(peer.id) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight)) if !visibleBounds.intersects(itemFrame) { continue } - - let peer = peers[i] - let itemId = AnyHashable(peer.id) validIds.append(itemId) var itemTransition = transition @@ -1050,13 +1054,13 @@ final class ShareWithPeersScreenComponent: Component { if case let .channel(channel) = peer { if case .broadcast = channel.info { if let count = component.stateContext.stateValue?.participants[peer.id] { - subtitle = environment.strings.Conversation_StatusSubscribers(Int32(count)) + subtitle = environment.strings.Conversation_StatusSubscribers(Int32(max(1, count))) } else { subtitle = environment.strings.Channel_Status } } else { if let count = component.stateContext.stateValue?.participants[peer.id] { - subtitle = environment.strings.Conversation_StatusMembers(Int32(count)) + subtitle = environment.strings.Conversation_StatusMembers(Int32(max(1, count))) } else { subtitle = environment.strings.Group_Status } @@ -1079,7 +1083,7 @@ final class ShareWithPeersScreenComponent: Component { } } - let _ = visibleItem.update( + let itemSize = visibleItem.update( transition: itemTransition, component: AnyComponent(PeerListItemComponent( context: component.context, @@ -1150,6 +1154,8 @@ final class ShareWithPeersScreenComponent: Component { environment: {}, containerSize: itemFrame.size ) + self.itemSizes[itemId] = itemSize + if let itemView = visibleItem.view { if itemView.superview == nil { self.itemContainerView.addSubview(itemView) @@ -1236,7 +1242,7 @@ final class ShareWithPeersScreenComponent: Component { controller.dismiss() } } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.35, curve: .spring))) }, secondaryAction: { [weak self] in guard let self, let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else { @@ -1389,13 +1395,13 @@ final class ShareWithPeersScreenComponent: Component { let subtitle: String? if case let .legacyGroup(group) = peer { - subtitle = environment.strings.Conversation_StatusMembers(Int32(group.participantCount)) + subtitle = environment.strings.Conversation_StatusMembers(Int32(max(1, group.participantCount))) } else if case let .channel(channel) = peer { if let count = stateValue.participants[peer.id] { if case .broadcast = channel.info { - subtitle = environment.strings.Conversation_StatusSubscribers(Int32(count)) + subtitle = environment.strings.Conversation_StatusSubscribers(Int32(max(1, count))) } else { - subtitle = environment.strings.Conversation_StatusMembers(Int32(count)) + subtitle = environment.strings.Conversation_StatusMembers(Int32(max(1, count))) } } else { subtitle = nil @@ -1433,7 +1439,7 @@ final class ShareWithPeersScreenComponent: Component { return } let update = { - let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring)) self.state?.updated(transition: transition) if self.searchStateContext != nil { @@ -1560,7 +1566,7 @@ final class ShareWithPeersScreenComponent: Component { } else { self.selectedOptions.remove(optionId) } - let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring)) self.state?.updated(transition: transition) self.presentOptionsTooltip(optionId: optionId) @@ -1696,7 +1702,7 @@ final class ShareWithPeersScreenComponent: Component { self.visibleSectionFooters.removeValue(forKey: id) } - let fadeTransition = Transition.easeInOut(duration: 0.25) + let fadeTransition = ComponentTransition.easeInOut(duration: 0.25) var searchQuery: String? var searchResultsAreEmpty = false @@ -1867,7 +1873,7 @@ final class ShareWithPeersScreenComponent: Component { } private var currentHasChannels: Bool? - func update(component: ShareWithPeersScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ShareWithPeersScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { guard !self.isDismissed else { return availableSize } @@ -2074,7 +2080,7 @@ final class ShareWithPeersScreenComponent: Component { if self.selectedCategories.isEmpty { self.selectedCategories.insert(.everyone) } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.35, curve: .spring))) }, isFocusedUpdated: { [weak self] isFocused in guard let self else { @@ -2118,7 +2124,7 @@ final class ShareWithPeersScreenComponent: Component { } self.searchStateContext = searchStateContext if applyState { - self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(contentReloaded: true))) + self.state?.updated(transition: ComponentTransition(animation: .none).withUserData(AnimationHint(contentReloaded: true))) } }) applyState = true @@ -2822,7 +2828,7 @@ final class ShareWithPeersScreenComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift index 6ec3c19b0ad..8b9f6ccbe8b 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift @@ -353,9 +353,6 @@ public extension ShareWithPeersScreen { } } if case let .channel(channel) = peer { - if channel.isForum { - return false - } if case .broadcast = channel.info { return false } @@ -496,9 +493,6 @@ public extension ShareWithPeersScreen { return true } } else if case let .channel(channel) = peer { - if channel.isForum { - return false - } if case .broadcast = channel.info { return false } diff --git a/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift b/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift index 52abc1f3283..081731cbb3d 100644 --- a/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift +++ b/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift @@ -12,6 +12,8 @@ public final class SliderComponent: Component { public let markPositions: Bool public let trackBackgroundColor: UIColor public let trackForegroundColor: UIColor + public let knobSize: CGFloat? + public let knobColor: UIColor? public let valueUpdated: (Int) -> Void public let isTrackingUpdated: ((Bool) -> Void)? @@ -21,6 +23,8 @@ public final class SliderComponent: Component { markPositions: Bool, trackBackgroundColor: UIColor, trackForegroundColor: UIColor, + knobSize: CGFloat? = nil, + knobColor: UIColor? = nil, valueUpdated: @escaping (Int) -> Void, isTrackingUpdated: ((Bool) -> Void)? = nil ) { @@ -29,6 +33,8 @@ public final class SliderComponent: Component { self.markPositions = markPositions self.trackBackgroundColor = trackBackgroundColor self.trackForegroundColor = trackForegroundColor + self.knobSize = knobSize + self.knobColor = knobColor self.valueUpdated = valueUpdated self.isTrackingUpdated = isTrackingUpdated } @@ -49,6 +55,12 @@ public final class SliderComponent: Component { if lhs.trackForegroundColor != rhs.trackForegroundColor { return false } + if lhs.knobSize != rhs.knobSize { + return false + } + if lhs.knobColor != rhs.knobColor { + return false + } return true } @@ -66,7 +78,7 @@ public final class SliderComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: SliderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: SliderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -94,8 +106,12 @@ public final class SliderComponent: Component { } else { sliderView = TGPhotoEditorSliderView() sliderView.enablePanHandling = true - sliderView.trackCornerRadius = 2.0 - sliderView.lineSize = 4.0 + if let knobSize = component.knobSize { + sliderView.lineSize = knobSize + 4.0 + } else { + sliderView.lineSize = 4.0 + } + sliderView.trackCornerRadius = sliderView.lineSize * 0.5 sliderView.dotSize = 5.0 sliderView.minimumValue = 0.0 sliderView.startValue = 0.0 @@ -110,12 +126,25 @@ public final class SliderComponent: Component { sliderView.backColor = component.trackBackgroundColor sliderView.startColor = component.trackBackgroundColor sliderView.trackColor = component.trackForegroundColor - sliderView.knobImage = generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 12.0, color: UIColor(white: 0.0, alpha: 0.25).cgColor) - context.setFillColor(UIColor.white.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0))) - }) + if let knobSize = component.knobSize { + sliderView.knobImage = generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 12.0, color: UIColor(white: 0.0, alpha: 0.25).cgColor) + if let knobColor = component.knobColor { + context.setFillColor(knobColor.cgColor) + } else { + context.setFillColor(UIColor.white.cgColor) + } + context.fillEllipse(in: CGRect(origin: CGPoint(x: floor((size.width - knobSize) * 0.5), y: floor((size.width - knobSize) * 0.5)), size: CGSize(width: knobSize, height: knobSize))) + }) + } else { + sliderView.knobImage = generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 12.0, color: UIColor(white: 0.0, alpha: 0.25).cgColor) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0))) + }) + } sliderView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size) sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX) @@ -153,7 +182,7 @@ public final class SliderComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/BUILD b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/BUILD new file mode 100644 index 00000000000..4eabbf93fc2 --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/BUILD @@ -0,0 +1,30 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "StarsAvatarComponent", + module_name = "StarsAvatarComponent", + 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/TelegramPresentationData", + "//submodules/PhotoResources", + "//submodules/AvatarNode", + "//submodules/AccountContext", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift new file mode 100644 index 00000000000..545bfc84095 --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift @@ -0,0 +1,362 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import ComponentFlow +import TelegramPresentationData +import PhotoResources +import AvatarNode +import AccountContext +import BundleIconComponent +import MultilineTextComponent + +public final class StarsAvatarComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let peer: StarsContext.State.Transaction.Peer + let photo: TelegramMediaWebFile? + let media: [Media] + let backgroundColor: UIColor + + public init(context: AccountContext, theme: PresentationTheme, peer: StarsContext.State.Transaction.Peer, photo: TelegramMediaWebFile?, media: [Media], backgroundColor: UIColor) { + self.context = context + self.theme = theme + self.peer = peer + self.photo = photo + self.media = media + self.backgroundColor = backgroundColor + } + + public static func ==(lhs: StarsAvatarComponent, rhs: StarsAvatarComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.photo != rhs.photo { + return false + } + if !areMediaArraysEqual(lhs.media, rhs.media) { + return false + } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + return true + } + + public final class View: UIView { + private let avatarNode: AvatarNode + private let backgroundView = UIImageView() + private let iconView = UIImageView() + private var imageNode: TransformImageNode? + private var imageFrameNode: UIView? + private var secondImageNode: TransformImageNode? + + private let fetchDisposable = DisposableSet() + + private var component: StarsAvatarComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0)) + + super.init(frame: frame) + + self.iconView.contentMode = .scaleAspectFit + + self.addSubnode(self.avatarNode) + self.addSubview(self.backgroundView) + self.addSubview(self.iconView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.fetchDisposable.dispose() + } + + func update(component: StarsAvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let size = CGSize(width: 40.0, height: 40.0) + var iconInset: CGFloat = 3.0 + var iconOffset: CGFloat = 0.0 + + var dimensions = size + + switch component.peer { + case let .peer(peer): + if !component.media.isEmpty { + let imageNode: TransformImageNode + var isFirstTime = false + if let current = self.imageNode { + imageNode = current + } else { + isFirstTime = true + imageNode = TransformImageNode() + imageNode.contentAnimations = [.subsequentUpdates] + self.addSubview(imageNode.view) + self.imageNode = imageNode + } + + if let image = component.media.first as? TelegramMediaImage { + if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions { + dimensions = imageDimensions.cgSize.aspectFilled(size) + } + if isFirstTime { + imageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false)) + self.fetchDisposable.add(chatMessagePhotoInteractiveFetched(context: component.context, userLocation: .other, photoReference: .standalone(media: image), displayAtSize: nil, storeToDownloadsPeerId: nil).startStrict()) + } + } else if let file = component.media.first as? TelegramMediaFile { + if let videoDimensions = file.dimensions { + dimensions = videoDimensions.cgSize.aspectFilled(size) + } + if isFirstTime { + imageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), autoFetchFullSizeThumbnail: true)) + } + } + + var imageFrame = CGRect(origin: .zero, size: size) + if component.media.count > 1 { + imageFrame = imageFrame.insetBy(dx: 2.0, dy: 2.0).offsetBy(dx: -2.0, dy: 2.0) + } + imageNode.frame = imageFrame + imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: dimensions, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() + + if component.media.count > 1 { + let secondImageNode: TransformImageNode + let imageFrameNode: UIView + var secondDimensions = size + var isFirstTime = false + if let current = self.secondImageNode, let currentFrame = self.imageFrameNode { + secondImageNode = current + imageFrameNode = currentFrame + } else { + isFirstTime = true + secondImageNode = TransformImageNode() + secondImageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] + self.insertSubview(secondImageNode.view, belowSubview: imageNode.view) + self.secondImageNode = secondImageNode + + imageFrameNode = UIView() + imageFrameNode.layer.cornerRadius = 8.0 + self.insertSubview(imageFrameNode, belowSubview: imageNode.view) + self.imageFrameNode = imageFrameNode + } + + if let image = component.media[1] as? TelegramMediaImage { + if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions { + secondDimensions = imageDimensions.cgSize.aspectFilled(size) + } + if isFirstTime { + secondImageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false)) + self.fetchDisposable.add(chatMessagePhotoInteractiveFetched(context: component.context, userLocation: .other, photoReference: .standalone(media: image), displayAtSize: nil, storeToDownloadsPeerId: nil).startStrict()) + } + } else if let file = component.media[1] as? TelegramMediaFile { + if let videoDimensions = file.dimensions { + secondDimensions = videoDimensions.cgSize.aspectFilled(size) + } + if isFirstTime { + secondImageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), useLargeThumbnail: true, autoFetchFullSizeThumbnail: true)) + } + } + + imageFrameNode.backgroundColor = component.backgroundColor + secondImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: secondDimensions, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() + secondImageNode.frame = imageFrame.offsetBy(dx: 4.0, dy: -4.0) + imageFrameNode.frame = imageFrame.insetBy(dx: -1.0 - UIScreenPixel, dy: -1.0 - UIScreenPixel) + } + + self.backgroundView.isHidden = true + self.iconView.isHidden = true + self.avatarNode.isHidden = true + } else if let photo = component.photo { + let imageNode: TransformImageNode + if let current = self.imageNode { + imageNode = current + } else { + imageNode = TransformImageNode() + imageNode.contentAnimations = [.subsequentUpdates] + self.addSubview(imageNode.view) + self.imageNode = imageNode + + imageNode.setSignal(chatWebFileImage(account: component.context.account, file: photo)) + self.fetchDisposable.add(chatMessageWebFileInteractiveFetched(account: component.context.account, userLocation: .other, image: photo).startStrict()) + } + + imageNode.frame = CGRect(origin: .zero, size: size) + imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() + + self.backgroundView.isHidden = true + self.iconView.isHidden = true + self.avatarNode.isHidden = true + } else { + self.avatarNode.setPeer( + context: component.context, + theme: component.theme, + peer: peer, + synchronousLoad: true + ) + self.backgroundView.isHidden = true + self.iconView.isHidden = true + self.avatarNode.isHidden = false + } + case .appStore: + self.backgroundView.image = generateGradientFilledCircleImage( + diameter: size.width, + colors: [ + UIColor(rgb: 0x2a9ef1).cgColor, + UIColor(rgb: 0x72d5fd).cgColor + ], + direction: .mirroredDiagonal + ) + self.backgroundView.isHidden = false + self.iconView.isHidden = false + self.avatarNode.isHidden = true + self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Apple") + case .playMarket: + self.backgroundView.image = generateGradientFilledCircleImage( + diameter: size.width, + colors: [ + UIColor(rgb: 0x54cb68).cgColor, + UIColor(rgb: 0xa0de7e).cgColor + ], + direction: .mirroredDiagonal + ) + self.backgroundView.isHidden = false + self.iconView.isHidden = false + self.avatarNode.isHidden = true + self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Google") + case .fragment: + self.backgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24)) + self.backgroundView.isHidden = false + self.iconView.isHidden = false + self.avatarNode.isHidden = true + self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment") + iconOffset = 2.0 + case .ads: + self.backgroundView.image = generateGradientFilledCircleImage( + diameter: size.width, + colors: [ + UIColor(rgb: 0xffa85c).cgColor, + UIColor(rgb: 0xffcd6a).cgColor + ], + direction: .mirroredDiagonal + ) + self.backgroundView.isHidden = false + self.iconView.isHidden = false + self.avatarNode.isHidden = true + self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white) + case .premiumBot: + iconInset = 7.0 + self.backgroundView.image = generateGradientFilledCircleImage( + diameter: size.width, + colors: [ + UIColor(rgb: 0x6b93ff).cgColor, + UIColor(rgb: 0x6b93ff).cgColor, + UIColor(rgb: 0x8d77ff).cgColor, + UIColor(rgb: 0xb56eec).cgColor, + UIColor(rgb: 0xb56eec).cgColor + ], + direction: .mirroredDiagonal + ) + self.backgroundView.isHidden = false + self.iconView.isHidden = false + self.avatarNode.isHidden = true + self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white) + case .unsupported: + iconInset = 7.0 + self.backgroundView.image = generateGradientFilledCircleImage( + diameter: size.width, + colors: [ + UIColor(rgb: 0xb1b1b1).cgColor, + UIColor(rgb: 0xcdcdcd).cgColor + ], + direction: .mirroredDiagonal + ) + self.backgroundView.isHidden = false + self.iconView.isHidden = false + self.avatarNode.isHidden = true + self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white) + } + + self.avatarNode.frame = CGRect(origin: .zero, size: size) + self.iconView.frame = CGRect(origin: .zero, size: size).insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset) + self.backgroundView.frame = CGRect(origin: .zero, size: size) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class StarsLabelComponent: CombinedComponent { + let text: NSAttributedString + + public init( + text: NSAttributedString + ) { + self.text = text + } + + public static func ==(lhs: StarsLabelComponent, rhs: StarsLabelComponent) -> Bool { + if lhs.text != rhs.text { + return false + } + return true + } + + public static var body: Body { + let text = Child(MultilineTextComponent.self) + let icon = Child(BundleIconComponent.self) + + return { context in + let component = context.component + + let text = text.update( + component: MultilineTextComponent(text: .plain(component.text)), + availableSize: CGSize(width: 100.0, height: 40.0), + transition: context.transition + ) + + let iconSize = CGSize(width: 20.0, height: 20.0) + let icon = icon.update( + component: BundleIconComponent( + name: "Premium/Stars/StarLarge", + tintColor: nil + ), + availableSize: iconSize, + transition: context.transition + ) + + let spacing: CGFloat = 3.0 + let totalWidth = text.size.width + spacing + iconSize.width + let size = CGSize(width: totalWidth, height: iconSize.height) + + context.add(text + .position(CGPoint(x: text.size.width / 2.0, y: size.height / 2.0)) + ) + context.add(icon + .position(CGPoint(x: totalWidth - iconSize.width / 2.0, y: size.height / 2.0 - UIScreenPixel)) + ) + return size + } + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD b/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD index 37eebaff13e..99fda0c389c 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD @@ -21,6 +21,7 @@ swift_library( "//submodules/PhotoResources", "//submodules/AvatarNode", "//submodules/AccountContext", + "//submodules/InvisibleInkDustNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift index 7ab97d78869..3e6c5fc93d6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift @@ -1,13 +1,16 @@ import Foundation import UIKit +import AsyncDisplayKit import Display import SwiftSignalKit +import Postbox import TelegramCore import ComponentFlow import TelegramPresentationData import PhotoResources import AvatarNode import AccountContext +import InvisibleInkDustNode final class StarsParticlesView: UIView { private struct Particle { @@ -245,24 +248,67 @@ public final class StarsImageComponent: Component { public enum Subject: Equatable { case none case photo(TelegramMediaWebFile) + case media([Media]) + case extendedMedia([TelegramExtendedMedia]) case transactionPeer(StarsContext.State.Transaction.Peer) + + public static func == (lhs: StarsImageComponent.Subject, rhs: StarsImageComponent.Subject) -> Bool { + switch lhs { + case .none: + if case .none = rhs { + return true + } else { + return false + } + case let .photo(lhsPhoto): + if case let .photo(rhsPhoto) = rhs, lhsPhoto == rhsPhoto { + return true + } else { + return false + } + case let .media(lhsMedia): + if case let .media(rhsMedia) = rhs, areMediaArraysEqual(lhsMedia, rhsMedia) { + return true + } else { + return false + } + case let .extendedMedia(lhsExtendedMedia): + if case let .extendedMedia(rhsExtendedMedia) = rhs, lhsExtendedMedia == rhsExtendedMedia { + return true + } else { + return false + } + case let .transactionPeer(lhsPeer): + if case let .transactionPeer(rhsPeer) = rhs, lhsPeer == rhsPeer { + return true + } else { + return false + } + } + } } public let context: AccountContext public let subject: Subject public let theme: PresentationTheme public let diameter: CGFloat + public let backgroundColor: UIColor + public let action: ((@escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? public init( context: AccountContext, subject: Subject, theme: PresentationTheme, - diameter: CGFloat + diameter: CGFloat, + backgroundColor: UIColor, + action: ((@escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? = nil ) { self.context = context self.subject = subject self.theme = theme self.diameter = diameter + self.backgroundColor = backgroundColor + self.action = action } public static func ==(lhs: StarsImageComponent, rhs: StarsImageComponent) -> Bool { @@ -272,24 +318,42 @@ public final class StarsImageComponent: Component { if lhs.subject != rhs.subject { return false } + if lhs.theme !== rhs.theme { + return false + } if lhs.diameter != rhs.diameter { return false } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } return true } public final class View: UIView { private var component: StarsImageComponent? + private var state: EmptyComponentState? private var smallParticlesView: StarsParticlesView? private var largeParticlesView: StarsParticlesView? + private var containerNode: ASDisplayNode? private var imageNode: TransformImageNode? + private var imageFrameNode: UIView? + private var secondImageNode: TransformImageNode? private var avatarNode: ImageNode? private var iconBackgroundView: UIImageView? private var iconView: UIImageView? + private var dustNode: MediaDustNode? + private var button: UIControl? + + private var lockView: UIImageView? + private var countView = ComponentView() private let fetchDisposable = MetaDisposable() + private var hiddenMediaDisposable: Disposable? + + private var hiddenMedia: [Media] = [] public override init(frame: CGRect) { super.init(frame: frame) @@ -300,10 +364,41 @@ public final class StarsImageComponent: Component { } deinit { self.fetchDisposable.dispose() + self.hiddenMediaDisposable?.dispose() + } + + @objc private func buttonPressed() { + guard let component = self.component else { + return + } + component.action?({ [weak self] media in + guard let self else { + return nil + } + return self.transitionNode(media) + }, { [weak self] view in + guard let self else { + return + } + self.superview?.addSubview(view) + }) + } + + public func transitionNode(_ transitionMedia: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + guard let component = self.component, let containerNode = self.containerNode else { + return nil + } + if case let .media(media) = component.subject, media.first?.id == transitionMedia.id { + return (containerNode, containerNode.bounds, { [weak containerNode] in + return (containerNode?.view.snapshotContentTree(unhide: true), nil) + }) + } + return nil } - func update(component: StarsImageComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: StarsImageComponent, state: EmptyComponentState, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component + self.state = state let smallParticlesView: StarsParticlesView if let current = self.smallParticlesView { @@ -329,8 +424,26 @@ public final class StarsImageComponent: Component { largeParticlesView.update(size: availableSize) largeParticlesView.frame = CGRect(origin: .zero, size: availableSize) - let imageSize = CGSize(width: component.diameter, height: component.diameter) - let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - imageSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - imageSize.height) / 2.0)), size: imageSize) + let containerNode: ASDisplayNode + if let current = self.containerNode { + containerNode = current + } else { + containerNode = ASDisplayNode() + + self.addSubview(containerNode.view) + self.containerNode = containerNode + } + + var imageSize = CGSize(width: component.diameter, height: component.diameter) + let containerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - imageSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - imageSize.height) / 2.0)), size: imageSize) + containerNode.frame = containerFrame + + if case let .media(media) = component.subject, media.count > 1 { + imageSize = CGSize(width: component.diameter - 6.0, height: component.diameter - 6.0) + } else if case let .extendedMedia(media) = component.subject, media.count > 1 { + imageSize = CGSize(width: component.diameter - 6.0, height: component.diameter - 6.0) + } + let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((containerFrame.width - imageSize.width) / 2.0), y: floorToScreenPixels((containerFrame.height - imageSize.height) / 2.0)), size: imageSize) switch component.subject { case .none: @@ -342,7 +455,7 @@ public final class StarsImageComponent: Component { } else { imageNode = TransformImageNode() imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] - self.addSubview(imageNode.view) + containerNode.view.addSubview(imageNode.view) self.imageNode = imageNode imageNode.setSignal(chatWebFileImage(account: component.context.account, file: photo)) @@ -351,6 +464,218 @@ public final class StarsImageComponent: Component { imageNode.frame = imageFrame imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() + case let .media(media): + let imageNode: TransformImageNode + var dimensions = imageSize + var isFirstTime = false + if let current = self.imageNode { + imageNode = current + } else { + isFirstTime = true + imageNode = TransformImageNode() + imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] + containerNode.view.addSubview(imageNode.view) + self.imageNode = imageNode + } + if let image = media.first as? TelegramMediaImage { + if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions { + dimensions = imageDimensions.cgSize.aspectFilled(imageSize) + } + if isFirstTime { + imageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false)) + } + } else if let file = media.first as? TelegramMediaFile { + if let videoDimensions = file.dimensions { + dimensions = videoDimensions.cgSize.aspectFilled(imageSize) + } + if isFirstTime { + imageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), useLargeThumbnail: true, autoFetchFullSizeThumbnail: true)) + } + } + imageNode.frame = imageFrame + imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: dimensions, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() + + if let firstMedia = media.first, self.hiddenMedia.contains(where: { $0.id == firstMedia.id }) { + containerNode.isHidden = true + } else { + containerNode.isHidden = false + } + + if media.count > 1 { + let secondImageNode: TransformImageNode + let imageFrameNode: UIView + var secondDimensions = imageSize + if let current = self.secondImageNode, let currentFrame = self.imageFrameNode { + secondImageNode = current + imageFrameNode = currentFrame + } else { + secondImageNode = TransformImageNode() + secondImageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] + containerNode.view.insertSubview(secondImageNode.view, belowSubview: imageNode.view) + self.secondImageNode = secondImageNode + + imageFrameNode = UIView() + imageFrameNode.layer.cornerRadius = 17.0 + containerNode.view.insertSubview(imageFrameNode, belowSubview: imageNode.view) + self.imageFrameNode = imageFrameNode + } + + if let image = media[1] as? TelegramMediaImage { + if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions { + secondDimensions = imageDimensions.cgSize.aspectFilled(imageSize) + } + if isFirstTime { + secondImageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false)) + } + } else if let file = media[1] as? TelegramMediaFile { + if let videoDimensions = file.dimensions { + secondDimensions = videoDimensions.cgSize.aspectFilled(imageSize) + } + if isFirstTime { + secondImageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), useLargeThumbnail: true, autoFetchFullSizeThumbnail: true)) + } + } + + imageFrameNode.backgroundColor = component.backgroundColor + secondImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: secondDimensions, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() + secondImageNode.frame = imageFrame.offsetBy(dx: 6.0, dy: -6.0) + imageFrameNode.frame = imageFrame.insetBy(dx: -2.0, dy: -2.0) + + let countSize = self.countView.update( + transition: .immediate, + component: AnyComponent( + Text(text: "\(media.count)", font: Font.with(size: 30.0, design: .round, weight: .medium), color: .white) + ), + environment: {}, + containerSize: imageFrame.size + ) + let countFrame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - countSize.width) / 2.0), y: imageFrame.minY + floorToScreenPixels((imageFrame.height - countSize.height) / 2.0)), size: countSize) + if let countView = self.countView.view { + if countView.superview == nil { + containerNode.view.addSubview(countView) + } + countView.frame = countFrame + } + } + case let .extendedMedia(extendedMedia): + let imageNode: TransformImageNode + let dustNode: MediaDustNode + var dimensions = imageSize + var isFirstTime = false + if let current = self.imageNode, let currentDust = self.dustNode { + imageNode = current + dustNode = currentDust + } else { + isFirstTime = true + imageNode = TransformImageNode() + imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] + containerNode.view.addSubview(imageNode.view) + self.imageNode = imageNode + + dustNode = MediaDustNode(enableAnimations: true) + dustNode.isUserInteractionEnabled = false + containerNode.view.addSubview(dustNode.view) + self.dustNode = dustNode + } + + let media: TelegramMediaImage + switch extendedMedia.first { + case let .preview(imageDimensions, immediateThumbnailData, _): + let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) + media = thumbnailMedia + if let imageDimensions { + dimensions = imageDimensions.cgSize.aspectFilled(imageSize) + } + default: + fatalError() + } + if isFirstTime { + imageNode.setSignal(chatSecretPhoto(account: component.context.account, userLocation: .other, photoReference: .standalone(media: media), ignoreFullSize: true, synchronousLoad: true)) + } + + imageNode.frame = imageFrame + imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: dimensions, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() + + dustNode.frame = imageFrame + dustNode.update(size: imageFrame.size, color: .white, transition: .immediate) + + if extendedMedia.count > 1 { + let secondImageNode: TransformImageNode + let imageFrameNode: UIView + var secondDimensions = imageSize + var isFirstTime = false + if let current = self.secondImageNode, let currentFrame = self.imageFrameNode { + secondImageNode = current + imageFrameNode = currentFrame + } else { + isFirstTime = true + secondImageNode = TransformImageNode() + secondImageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] + containerNode.view.insertSubview(secondImageNode.view, belowSubview: imageNode.view) + self.secondImageNode = secondImageNode + + imageFrameNode = UIView() + imageFrameNode.layer.cornerRadius = 17.0 + containerNode.view.insertSubview(imageFrameNode, belowSubview: imageNode.view) + self.imageFrameNode = imageFrameNode + } + + let media: TelegramMediaImage + switch extendedMedia[1] { + case let .preview(imageDimensions, immediateThumbnailData, _): + let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) + media = thumbnailMedia + if let imageDimensions { + secondDimensions = imageDimensions.cgSize.aspectFilled(imageSize) + } + default: + fatalError() + } + + if isFirstTime { + secondImageNode.setSignal(chatSecretPhoto(account: component.context.account, userLocation: .other, photoReference: .standalone(media: media), ignoreFullSize: true, synchronousLoad: true)) + } + + imageFrameNode.backgroundColor = component.backgroundColor + secondImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: secondDimensions, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() + secondImageNode.frame = imageFrame.offsetBy(dx: 6.0, dy: -6.0) + imageFrameNode.frame = imageFrame.insetBy(dx: -2.0, dy: -2.0) + } + + var totalLabelWidth: CGFloat = 0.0 + let labelSpacing: CGFloat = 4.0 + let lockView: UIImageView + if let current = self.lockView { + lockView = current + } else { + lockView = UIImageView(image: UIImage(bundleImageName: "Premium/Stars/MediaLock")) + containerNode.view.addSubview(lockView) + } + if let icon = lockView.image { + totalLabelWidth += icon.size.width + } + + if extendedMedia.count > 1 { + let countSize = self.countView.update( + transition: .immediate, + component: AnyComponent( + Text(text: "\(extendedMedia.count)", font: Font.with(size: 30.0, design: .round, weight: .medium), color: .white) + ), + environment: {}, + containerSize: imageFrame.size + ) + let iconWidth = totalLabelWidth + totalLabelWidth += countSize.width + labelSpacing + let countFrame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - totalLabelWidth) / 2.0) + iconWidth + labelSpacing, y: imageFrame.minY + floorToScreenPixels((imageFrame.height - countSize.height) / 2.0)), size: countSize) + if let countView = self.countView.view { + if countView.superview == nil { + containerNode.view.addSubview(countView) + } + countView.frame = countFrame + } + } + + lockView.frame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - totalLabelWidth) / 2.0), y: imageFrame.minY + floorToScreenPixels((imageFrame.height - lockView.bounds.height) / 2.0)), size: lockView.bounds.size) case let .transactionPeer(peer): if case let .peer(peer) = peer { let avatarNode: ImageNode @@ -359,7 +684,7 @@ public final class StarsImageComponent: Component { } else { avatarNode = ImageNode() avatarNode.displaysAsynchronously = false - self.addSubview(avatarNode.view) + containerNode.view.addSubview(avatarNode.view) self.avatarNode = avatarNode avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: peer, size: imageSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true)) @@ -375,8 +700,8 @@ public final class StarsImageComponent: Component { iconBackgroundView = UIImageView() iconView = UIImageView() - self.addSubview(iconBackgroundView) - self.addSubview(iconView) + containerNode.view.addSubview(iconBackgroundView) + containerNode.view.addSubview(iconView) self.iconBackgroundView = iconBackgroundView self.iconView = iconView @@ -412,6 +737,16 @@ public final class StarsImageComponent: Component { ) iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment") iconOffset = 5.0 + case .ads: + iconBackgroundView.image = generateGradientFilledCircleImage( + diameter: imageSize.width, + colors: [ + UIColor(rgb: 0xffa85c).cgColor, + UIColor(rgb: 0xffcd6a).cgColor + ], + direction: .mirroredDiagonal + ) + iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white) case .premiumBot: iconInset = 15.0 iconBackgroundView.image = generateGradientFilledCircleImage( @@ -442,6 +777,40 @@ public final class StarsImageComponent: Component { iconView.frame = imageFrame.insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset) } } + + if let _ = component.action { + if self.button == nil { + let button = UIControl(frame: imageFrame) + button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) + containerNode.view.addSubview(button) + self.button = button + } + } else if let button = self.button { + self.button = nil + button.removeFromSuperview() + } + + if case .media = component.subject { + if self.hiddenMediaDisposable == nil { + self.hiddenMediaDisposable = component.context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in + guard let self, let component = self.component else { + return + } + var hiddenMedia: [Media] = [] + for id in ids { + if case let .chat(accountId, _, media) = id, accountId == component.context.account.id { + hiddenMedia.append(media) + } + } + self.hiddenMedia = hiddenMedia + self.state?.updated() + }).strict() + } + } else if let hiddenMediaDisposable = self.hiddenMediaDisposable { + self.hiddenMediaDisposable = nil + hiddenMediaDisposable.dispose() + } + return availableSize } } @@ -450,7 +819,7 @@ public final class StarsImageComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - return view.update(component: self, availableSize: availableSize, transition: transition) + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, state: state, availableSize: availableSize, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/ItemLoadingComponent.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/ItemLoadingComponent.swift index 24905d0bf9f..96600232dd6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/ItemLoadingComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/ItemLoadingComponent.swift @@ -63,7 +63,7 @@ final class ItemLoadingComponent: Component { }) } - func update(component: ItemLoadingComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ItemLoadingComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let isFirstTime = self.component == nil self.component = component @@ -86,7 +86,7 @@ final class ItemLoadingComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index 56175544ac3..3808503034e 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -61,7 +61,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { let forceDark: Bool let products: [StarsProduct]? let expanded: Bool - let stateUpdated: (Transition) -> Void + let stateUpdated: (ComponentTransition) -> Void let buy: (StarsProduct) -> Void init( @@ -76,7 +76,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { forceDark: Bool, products: [StarsProduct]?, expanded: Bool, - stateUpdated: @escaping (Transition) -> Void, + stateUpdated: @escaping (ComponentTransition) -> Void, buy: @escaping (StarsProduct) -> Void ) { self.context = context @@ -229,7 +229,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { let textString: String if let _ = context.component.requiredStars { - textString = strings.Stars_Purchase_StarsNeededInfo(state.peer?.compactDisplayTitle ?? "").string + textString = state.peer == nil ? strings.Stars_Purchase_StarsNeededUnlockInfo : strings.Stars_Purchase_StarsNeededInfo(state.peer?.compactDisplayTitle ?? "").string } else { textString = strings.Stars_Purchase_GetStarsInfo } @@ -310,11 +310,11 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { minimumCount = requiredStars - balance } for product in products { - if let minimumCount, minimumCount > product.option.count { + if let minimumCount, minimumCount > product.option.count && !(items.isEmpty && product.id == products.last?.id) { continue } - if let _ = minimumCount, items.isEmpty { + if let _ = minimumCount, items.isEmpty { } else if !context.component.expanded && !initialValues.contains(product.option.count) { continue @@ -369,7 +369,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { }, highlighting: .disabled, updateIsHighlighted: { view, isHighlighted in - let transition: Transition = .easeInOut(duration: 0.25) + let transition: ComponentTransition = .easeInOut(duration: 0.25) if let superview = view.superview { transition.setScale(view: superview, scale: isHighlighted ? 0.9 : 1.0) } @@ -381,7 +381,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { } } - if !context.component.expanded { + if !context.component.expanded && items.count > 1 { let titleComponent = AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: strings.Stars_Purchase_ShowMore, diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD new file mode 100644 index 00000000000..f392b1ca5e9 --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD @@ -0,0 +1,40 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "StarsTransactionScreen", + module_name = "StarsTransactionScreen", + 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/TelegramStringFormatting", + "//submodules/PresentationDataUtils", + "//submodules/Components/SheetComponent", + "//submodules/UndoUI", + "//submodules/TextFormat", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/SolidRoundedButtonComponent", + "//submodules/AvatarNode", + "//submodules/TelegramUI/Components/Stars/StarsImageComponent", + "//submodules/GalleryUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift similarity index 83% rename from submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift rename to submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index a3e2450ba23..b73fa35bdbd 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import Display import AsyncDisplayKit +import Postbox import TelegramCore import SwiftSignalKit import AccountContext @@ -20,6 +21,7 @@ import TextFormat import TelegramStringFormatting import UndoUI import StarsImageComponent +import GalleryUI private final class StarsTransactionSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -29,7 +31,9 @@ private final class StarsTransactionSheetContent: CombinedComponent { let action: () -> Void let cancel: (Bool) -> Void let openPeer: (EnginePeer) -> Void - let copyTransactionId: () -> Void + let openMessage: (EngineMessage.Id) -> Void + let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void + let copyTransactionId: (String) -> Void init( context: AccountContext, @@ -37,13 +41,17 @@ private final class StarsTransactionSheetContent: CombinedComponent { action: @escaping () -> Void, cancel: @escaping (Bool) -> Void, openPeer: @escaping (EnginePeer) -> Void, - copyTransactionId: @escaping () -> Void + openMessage: @escaping (EngineMessage.Id) -> Void, + openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void, + copyTransactionId: @escaping (String) -> Void ) { self.context = context self.subject = subject self.action = action self.cancel = cancel self.openPeer = openPeer + self.openMessage = openMessage + self.openMedia = openMedia self.copyTransactionId = copyTransactionId } @@ -75,7 +83,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { var peerIds: [EnginePeer.Id] = [] switch subject { - case let .transaction(transaction): + case let .transaction(transaction, _): if case let .peer(peer) = transaction.peer { peerIds.append(peer.id) } @@ -172,17 +180,23 @@ private final class StarsTransactionSheetContent: CombinedComponent { let transactionId: String? let date: Int32 let via: String? + let messageId: EngineMessage.Id? let toPeer: EnginePeer? let transactionPeer: StarsContext.State.Transaction.Peer? + let media: [Media] let photo: TelegramMediaWebFile? let isRefund: Bool var delayedCloseOnOpenPeer = true switch subject { - case let .transaction(transaction): + case let .transaction(transaction, parentPeer): switch transaction.peer { case let .peer(peer): - titleText = transaction.title ?? peer.compactDisplayTitle + if !transaction.media.isEmpty { + titleText = strings.Stars_Transaction_MediaPurchase + } else { + titleText = transaction.title ?? peer.compactDisplayTitle + } via = nil case .appStore: titleText = strings.Stars_Transaction_AppleTopUp_Title @@ -194,13 +208,52 @@ private final class StarsTransactionSheetContent: CombinedComponent { titleText = strings.Stars_Transaction_PremiumBotTopUp_Title via = strings.Stars_Transaction_PremiumBotTopUp_Subtitle case .fragment: - titleText = strings.Stars_Transaction_FragmentTopUp_Title - via = strings.Stars_Transaction_FragmentTopUp_Subtitle + if parentPeer.id == component.context.account.peerId { + titleText = strings.Stars_Transaction_FragmentTopUp_Title + via = strings.Stars_Transaction_FragmentTopUp_Subtitle + } else { + titleText = strings.Stars_Transaction_FragmentWithdrawal_Title + via = strings.Stars_Transaction_FragmentWithdrawal_Subtitle + } + case .ads: + titleText = strings.Stars_Transaction_TelegramAds_Title + via = strings.Stars_Transaction_TelegramAds_Subtitle case .unsupported: titleText = strings.Stars_Transaction_Unsupported_Title via = nil } - descriptionText = transaction.description ?? "" + if !transaction.media.isEmpty { + var description: String = "" + var photoCount: Int32 = 0 + var videoCount: Int32 = 0 + for media in transaction.media { + if let _ = media as? TelegramMediaFile { + videoCount += 1 + } else { + photoCount += 1 + } + } + if photoCount > 0 && videoCount > 0 { + description += strings.Stars_Transaction_MediaAnd(strings.Stars_Transaction_Photos(photoCount), strings.Stars_Transaction_Videos(videoCount)).string + } else if photoCount > 0 { + if photoCount > 1 { + description += strings.Stars_Transaction_Photos(photoCount) + } else { + description += strings.Stars_Transaction_SinglePhoto + } + } else if videoCount > 0 { + if videoCount > 1 { + description += strings.Stars_Transaction_Videos(videoCount) + } else { + description += strings.Stars_Transaction_SingleVideo + } + } + descriptionText = description + } else { + descriptionText = transaction.description ?? "" + } + + messageId = transaction.paidMessageId count = transaction.count transactionId = transaction.id @@ -211,6 +264,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { toPeer = nil } transactionPeer = transaction.peer + media = transaction.media photo = transaction.photo isRefund = transaction.flags.contains(.isRefund) case let .receipt(receipt): @@ -218,6 +272,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { descriptionText = receipt.invoiceMedia.description count = (receipt.invoice.prices.first?.amount ?? receipt.invoiceMedia.totalAmount) * -1 via = nil + messageId = nil transactionId = receipt.transactionId date = receipt.date if let peer = state.peerMap[receipt.botPaymentId] { @@ -226,6 +281,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { toPeer = nil } transactionPeer = nil + media = [] photo = receipt.invoiceMedia.photo isRefund = false delayedCloseOnOpenPeer = false @@ -256,7 +312,9 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) let imageSubject: StarsImageComponent.Subject - if let photo { + if !media.isEmpty { + imageSubject = .media(media) + } else if let photo { imageSubject = .photo(photo) } else if let transactionPeer { imageSubject = .transactionPeer(transactionPeer) @@ -270,7 +328,11 @@ private final class StarsTransactionSheetContent: CombinedComponent { context: component.context, subject: imageSubject, theme: theme, - diameter: 90.0 + diameter: 90.0, + backgroundColor: theme.actionSheet.opaqueItemBackgroundColor, + action: !media.isEmpty ? { transitionNode, addToTransitionSurface in + component.openMedia(media, transitionNode, addToTransitionSurface) + } : nil ), availableSize: CGSize(width: context.availableSize.width, height: 200.0), transition: .immediate @@ -340,6 +402,40 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) )) } + + if let messageId { + let peerName: String + if case let .transaction(_, parentPeer) = component.subject { + if parentPeer.id == component.context.account.peerId { + if let toPeer { + peerName = toPeer.addressName ?? "c/\(toPeer.id.id._internalGetInt64Value())" + } else { + peerName = "" + } + } else { + peerName = parentPeer.addressName ?? "c/\(parentPeer.id.id._internalGetInt64Value())" + } + } else { + peerName = "" + } + tableItems.append(.init( + id: "media", + title: strings.Stars_Transaction_Media, + component: AnyComponent( + Button( + content: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "t.me/\(peerName)/\(messageId.id)", font: tableFont, textColor: tableLinkColor))) + ), + action: { + component.openMessage(messageId) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + ) + ) + )) + } if let transactionId { tableItems.append(.init( @@ -355,7 +451,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) ), action: { - component.copyTransactionId() + component.copyTransactionId(transactionId) } ) ), @@ -539,19 +635,25 @@ private final class StarsTransactionSheetComponent: CombinedComponent { let subject: StarsTransactionScreen.Subject let action: () -> Void let openPeer: (EnginePeer) -> Void - let copyTransactionId: () -> Void + let openMessage: (EngineMessage.Id) -> Void + let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void + let copyTransactionId: (String) -> Void init( context: AccountContext, subject: StarsTransactionScreen.Subject, action: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void, - copyTransactionId: @escaping () -> Void + openMessage: @escaping (EngineMessage.Id) -> Void, + openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void, + copyTransactionId: @escaping (String) -> Void ) { self.context = context self.subject = subject self.action = action self.openPeer = openPeer + self.openMessage = openMessage + self.openMedia = openMedia self.copyTransactionId = copyTransactionId } @@ -594,6 +696,8 @@ private final class StarsTransactionSheetComponent: CombinedComponent { } }, openPeer: context.component.openPeer, + openMessage: context.component.openMessage, + openMedia: context.component.openMedia, copyTransactionId: context.component.copyTransactionId )), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), @@ -662,7 +766,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { public class StarsTransactionScreen: ViewControllerComponentContainer { public enum Subject: Equatable { - case transaction(StarsContext.State.Transaction) + case transaction(StarsContext.State.Transaction, EnginePeer) case receipt(BotPaymentReceipt) } @@ -680,7 +784,9 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { self.context = context var openPeerImpl: ((EnginePeer) -> Void)? - var copyTransactionIdImpl: (() -> Void)? + var openMessageImpl: ((EngineMessage.Id) -> Void)? + var openMediaImpl: (([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? + var copyTransactionIdImpl: ((String) -> Void)? super.init( context: context, component: StarsTransactionSheetComponent( @@ -690,8 +796,14 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { openPeer: { peerId in openPeerImpl?(peerId) }, - copyTransactionId: { - copyTransactionIdImpl?() + openMessage: { messageId in + openMessageImpl?(messageId) + }, + openMedia: { media, transitionNode, addToTransitionSurface in + openMediaImpl?(media, transitionNode, addToTransitionSurface) + }, + copyTransactionId: { transactionId in + copyTransactionIdImpl?(transactionId) } ), navigationBarAppearance: .none, @@ -719,10 +831,70 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { }) } - copyTransactionIdImpl = { [weak self] in + openMessageImpl = { [weak self] messageId in + guard let self else { + return + } + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId) + ) + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer else { + return + } + if let navigationController = self.navigationController as? NavigationController { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil)) + } + }) + } + + openMediaImpl = { [weak self] media, transitionNode, addToTransitionSurface in guard let self else { return } + + let message = Message( + stableId: 0, + stableVersion: 0, + id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(0)), namespace: Namespaces.Message.Local, id: 0), + globallyUniqueId: 0, + groupingKey: nil, + groupInfo: nil, + threadId: nil, + timestamp: 0, + flags: [], + tags: [], + globalTags: [], + localTags: [], + customTags: [], + forwardInfo: nil, + author: nil, + text: "", + attributes: [], + media: [TelegramMediaPaidContent(amount: 0, extendedMedia: media.map { .full(media: $0) })], + peers: SimpleDictionary(), + associatedMessages: SimpleDictionary(), + associatedMessageIds: [], + associatedMedia: [:], + associatedThreadInfo: nil, + associatedStories: [:] + ) + let gallery = GalleryController(context: self.context, source: .standaloneMessage(message, 0), replaceRootController: { _, _ in + }, baseNavigationController: nil) + self.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { messageId, media in + if let transitionNode = transitionNode(media) { + return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: addToTransitionSurface) + } + return nil + })) + } + + copyTransactionIdImpl = { [weak self] transactionId in + guard let self else { + return + } + UIPasteboard.general.string = transactionId + self.dismissAllTooltips() let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -1035,7 +1207,7 @@ private final class PeerCellComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: PeerCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PeerCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -1081,7 +1253,7 @@ private final class PeerCellComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -1125,7 +1297,7 @@ private final class TransactionCellComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: TransactionCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: TransactionCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -1201,7 +1373,7 @@ private final class TransactionCellComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD index 518c2783e2c..2647ba93b58 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD @@ -33,6 +33,7 @@ swift_library( "//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/TelegramUI/Components/ScrollComponent", "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", + "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/Components/BlurredBackgroundComponent", "//submodules/Components/BundleIconComponent", "//submodules/Components/SolidRoundedButtonComponent", @@ -40,6 +41,12 @@ swift_library( "//submodules/AvatarNode", "//submodules/PhotoResources", "//submodules/TelegramUI/Components/Stars/StarsImageComponent", + "//submodules/PasswordSetupUI", + "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", + "//submodules/TelegramUI/Components/ListItemComponentAdaptor", + "//submodules/StatisticsUI", + "//submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen", + "//submodules/TelegramUI/Components/Stars/StarsAvatarComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift index 66546529efe..243e28cd2a6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift @@ -7,30 +7,47 @@ import AccountContext import MultilineTextComponent import TelegramPresentationData import PresentationDataUtils -import SolidRoundedButtonComponent +import ButtonComponent +import BundleIconComponent +import TelegramStringFormatting final class StarsBalanceComponent: Component { let theme: PresentationTheme let strings: PresentationStrings let dateTimeFormat: PresentationDateTimeFormat let count: Int64 - let purchaseAvailable: Bool - let buy: () -> Void + let rate: Double? + let actionTitle: String + let actionAvailable: Bool + let actionIsEnabled: Bool + let actionCooldownUntilTimestamp: Int32? + let action: () -> Void + let buyAds: (() -> Void)? init( theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, count: Int64, - purchaseAvailable: Bool, - buy: @escaping () -> Void + rate: Double?, + actionTitle: String, + actionAvailable: Bool, + actionIsEnabled: Bool, + actionCooldownUntilTimestamp: Int32? = nil, + action: @escaping () -> Void, + buyAds: (() -> Void)? ) { self.theme = theme self.strings = strings self.dateTimeFormat = dateTimeFormat self.count = count - self.purchaseAvailable = purchaseAvailable - self.buy = buy + self.rate = rate + self.actionTitle = actionTitle + self.actionAvailable = actionAvailable + self.actionIsEnabled = actionIsEnabled + self.actionCooldownUntilTimestamp = actionCooldownUntilTimestamp + self.action = action + self.buyAds = buyAds } static func ==(lhs: StarsBalanceComponent, rhs: StarsBalanceComponent) -> Bool { @@ -43,12 +60,24 @@ final class StarsBalanceComponent: Component { if lhs.dateTimeFormat != rhs.dateTimeFormat { return false } - if lhs.purchaseAvailable != rhs.purchaseAvailable { + if lhs.actionTitle != rhs.actionTitle { + return false + } + if lhs.actionAvailable != rhs.actionAvailable { + return false + } + if lhs.actionIsEnabled != rhs.actionIsEnabled { + return false + } + if lhs.actionCooldownUntilTimestamp != rhs.actionCooldownUntilTimestamp { return false } if lhs.count != rhs.count { return false } + if lhs.rate != rhs.rate { + return false + } return true } @@ -57,8 +86,12 @@ final class StarsBalanceComponent: Component { private let title = ComponentView() private let subtitle = ComponentView() private var button = ComponentView() + private var buyAdsButton = ComponentView() private var component: StarsBalanceComponent? + private weak var state: EmptyComponentState? + + private var timer: Timer? override init(frame: CGRect) { super.init(frame: frame) @@ -72,8 +105,31 @@ final class StarsBalanceComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: StarsBalanceComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StarsBalanceComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component + self.state = state + + var remainingCooldownSeconds: Int32 = 0 + if let cooldownUntilTimestamp = component.actionCooldownUntilTimestamp { + remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) + remainingCooldownSeconds = max(0, remainingCooldownSeconds) + } + + if remainingCooldownSeconds > 0 { + if self.timer == nil { + self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + self.state?.updated(transition: .immediate) + }) + } + } else { + if let timer = self.timer { + self.timer = nil + timer.invalidate() + } + } let sideInset: CGFloat = 16.0 var contentHeight: CGFloat = sideInset @@ -105,11 +161,18 @@ final class StarsBalanceComponent: Component { } contentHeight += titleSize.height + let subtitleText: String + if let rate = component.rate { + subtitleText = "≈\(formatUsdValue(component.count, rate: rate))" + } else { + subtitleText = component.strings.Stars_Intro_YourBalance + } + let subtitleSize = self.subtitle.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: component.strings.Stars_Intro_YourBalance, font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)), + text: .plain(NSAttributedString(string: subtitleText, font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)), horizontalAlignment: .center ) ), @@ -125,24 +188,52 @@ final class StarsBalanceComponent: Component { } contentHeight += subtitleSize.height - if component.purchaseAvailable { + if component.actionAvailable { contentHeight += 12.0 + var actionTitle = component.actionTitle + var withdrawWidth = availableSize.width - sideInset * 2.0 + if let _ = component.buyAds { + withdrawWidth = (withdrawWidth - 10.0) / 2.0 + actionTitle = component.strings.Stars_BotRevenue_Withdraw_WithdrawShort + } + + let content: AnyComponentWithIdentity + if remainingCooldownSeconds > 0 { + content = AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent( + VStack([ + AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(Text(text: actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))), + AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(HStack([ + AnyComponentWithIdentity(id: 1, component: AnyComponent(BundleIconComponent(name: "Chat List/StatusLockIcon", tintColor: component.theme.list.itemCheckColors.fillColor.mixedWith(component.theme.list.itemCheckColors.foregroundColor, alpha: 0.7)))), + AnyComponentWithIdentity(id: 0, component: AnyComponent(Text(text: stringForRemainingTime(remainingCooldownSeconds), font: Font.with(size: 11.0, weight: .medium, traits: [.monospacedNumbers]), color: component.theme.list.itemCheckColors.fillColor.mixedWith(component.theme.list.itemCheckColors.foregroundColor, alpha: 0.7)))) + ], spacing: 3.0))) + ], spacing: 1.0) + )) + } else { + content = AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))) + } + let buttonSize = self.button.update( - transition: .immediate, - component: AnyComponent( - SolidRoundedButtonComponent( - title: component.strings.Stars_Intro_Buy, - theme: SolidRoundedButtonComponent.Theme(theme: component.theme), - height: 50.0, - cornerRadius: 11.0, - action: { [weak self] in - self?.component?.buy() + 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.8) + ), + content: content, + isEnabled: component.actionIsEnabled, + allowActionWhenDisabled: false, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return } - ) - ), + component.action() + } + )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + containerSize: CGSize(width: withdrawWidth, height: 50.0) ) if let buttonView = self.button.view { if buttonView.superview == nil { @@ -151,6 +242,40 @@ final class StarsBalanceComponent: Component { let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize) buttonView.frame = buttonFrame } + + if let _ = component.buyAds { + let buttonSize = self.buyAdsButton.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.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: component.strings.Stars_BotRevenue_Withdraw_BuyAds, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))), + isEnabled: component.actionIsEnabled, + allowActionWhenDisabled: false, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.buyAds?() + } + )), + environment: {}, + containerSize: CGSize(width: withdrawWidth, height: 50.0) + ) + if let buttonView = self.buyAdsButton.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + let buttonFrame = CGRect(origin: CGPoint(x: sideInset + withdrawWidth + 10.0, y: contentHeight), size: buttonSize) + buttonView.frame = buttonFrame + } + } + + contentHeight += buttonSize.height } contentHeight += sideInset @@ -163,7 +288,20 @@ final class StarsBalanceComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +func stringForRemainingTime(_ duration: Int32) -> String { + let hours = duration / 3600 + let minutes = duration / 60 % 60 + let seconds = duration % 60 + let durationString: String + if hours > 0 { + durationString = String(format: "%d:%02d", hours, minutes) + } else { + durationString = String(format: "%02d:%02d", minutes, seconds) + } + return durationString +} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift new file mode 100644 index 00000000000..dfe6aeafabf --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift @@ -0,0 +1,150 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import AccountContext +import MultilineTextComponent +import TelegramPresentationData +import PresentationDataUtils + +final class StarsOverviewItemComponent: Component { + let theme: PresentationTheme + let dateTimeFormat: PresentationDateTimeFormat + let title: String + let value: Int64 + let rate: Double + + init( + theme: PresentationTheme, + dateTimeFormat: PresentationDateTimeFormat, + title: String, + value: Int64, + rate: Double + ) { + self.theme = theme + self.dateTimeFormat = dateTimeFormat + self.title = title + self.value = value + self.rate = rate + } + + static func ==(lhs: StarsOverviewItemComponent, rhs: StarsOverviewItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.dateTimeFormat != rhs.dateTimeFormat { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.rate != rhs.rate { + return false + } + return true + } + + final class View: UIView { + private let icon = UIImageView() + private let value = ComponentView() + private let usdValue = ComponentView() + private let title = ComponentView() + + private var component: StarsOverviewItemComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.icon.image = UIImage(bundleImageName: "Premium/Stars/StarMedium") + + self.addSubview(self.icon) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: StarsOverviewItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let sideInset: CGFloat = 16.0 + + var valueOffset: CGFloat = 0.0 + if let icon = self.icon.image { + self.icon.frame = CGRect(origin: CGPoint(x: sideInset - 1.0, y: 10.0), size: icon.size) + valueOffset += icon.size.width + } + + let valueString = presentationStringsFormattedNumber(Int32(component.value), component.dateTimeFormat.groupingSeparator) + let usdValueString = formatUsdValue(component.value, rate: component.rate) + + let valueSize = self.value.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: valueString, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)) + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let valueFrame = CGRect(origin: CGPoint(x: sideInset + valueOffset + 2.0, y: 10.0), size: valueSize) + if let valueView = self.value.view { + if valueView.superview == nil { + self.addSubview(valueView) + } + valueView.frame = valueFrame + } + + let usdValueSize = self.usdValue.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: "≈\(usdValueString)", font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let usdValueFrame = CGRect(origin: CGPoint(x: sideInset + valueOffset + valueSize.width + 6.0, y: 14.0), size: usdValueSize) + if let usdValueView = self.usdValue.view { + if usdValueView.superview == nil { + self.addSubview(usdValueView) + } + usdValueView.frame = usdValueFrame + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + let titleFrame = CGRect(origin: CGPoint(x: sideInset, y: 32.0), size: titleSize) + titleView.frame = titleFrame + } + + return CGSize(width: availableSize.width, height: 59.0) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift new file mode 100644 index 00000000000..de36ea1c50c --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift @@ -0,0 +1,795 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import Postbox +import MultilineTextComponent +import BalancedTextComponent +import Markdown +import ListSectionComponent +import BundleIconComponent +import TextFormat +import UndoUI +import ListItemComponentAdaptor +import StatisticsUI +import ItemListUI +import StarsWithdrawalScreen + +final class StarsStatisticsScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let peerId: EnginePeer.Id + let revenueContext: StarsRevenueStatsContext + let transactionsContext: StarsTransactionsContext + let openTransaction: (StarsContext.State.Transaction) -> Void + let withdraw: () -> Void + let showTimeoutTooltip: (Int32) -> Void + let buyAds: () -> Void + + init( + context: AccountContext, + peerId: EnginePeer.Id, + revenueContext: StarsRevenueStatsContext, + transactionsContext: StarsTransactionsContext, + openTransaction: @escaping (StarsContext.State.Transaction) -> Void, + withdraw: @escaping () -> Void, + showTimeoutTooltip: @escaping (Int32) -> Void, + buyAds: @escaping () -> Void + ) { + self.context = context + self.peerId = peerId + self.revenueContext = revenueContext + self.transactionsContext = transactionsContext + self.openTransaction = openTransaction + self.withdraw = withdraw + self.showTimeoutTooltip = showTimeoutTooltip + self.buyAds = buyAds + } + + static func ==(lhs: StarsStatisticsScreenComponent, rhs: StarsStatisticsScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peerId != rhs.peerId { + return false + } + if lhs.revenueContext !== rhs.revenueContext { + return false + } + return true + } + + private final class ScrollViewImpl: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if let _ = otherGestureRecognizer as? UIPanGestureRecognizer { + return true + } + return false + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPanGestureRecognizer, let gestureRecognizers = gestureRecognizer.view?.gestureRecognizers { + for otherGestureRecognizer in gestureRecognizers { + if otherGestureRecognizer !== gestureRecognizer, let panGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer, panGestureRecognizer.minimumNumberOfTouches == 2 { + return gestureRecognizer.numberOfTouches < 2 + } + } + + if let view = gestureRecognizer.view?.hitTest(gestureRecognizer.location(in: gestureRecognizer.view), with: nil) as? UIControl { + return !view.isTracking + } + + return true + } else { + return true + } + } + } + + class View: UIView, UIScrollViewDelegate { + private let scrollView: ScrollViewImpl + + private var currentSelectedPanelId: AnyHashable? + + private let navigationBackgroundView: BlurredBackgroundView + private let navigationSeparatorLayer: SimpleLayer + private let navigationSeparatorLayerContainer: SimpleLayer + + private let headerView = ComponentView() + private let headerOffsetContainer: UIView + + private let scrollContainerView: UIView + + private let titleView = ComponentView() + + private let chartView = ComponentView() + private let proceedsView = ComponentView() + private let balanceView = ComponentView() + + private let transactionsHeader = ComponentView() + private let transactionsBackground = UIView() + private let transactionsView = ComponentView() + + private var component: StarsStatisticsScreenComponent? + private weak var state: EmptyComponentState? + private var environment: Environment? + private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)? + private var controller: (() -> ViewController?)? + + private var ignoreScrolling: Bool = false + + private var stateDisposable: Disposable? + private var starsState: StarsRevenueStats? + + private var previousBalance: Int64? + + private var cachedChevronImage: (UIImage, PresentationTheme)? + + override init(frame: CGRect) { + self.headerOffsetContainer = UIView() + self.headerOffsetContainer.isUserInteractionEnabled = false + + self.navigationBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true) + self.navigationBackgroundView.alpha = 0.0 + + self.navigationSeparatorLayer = SimpleLayer() + self.navigationSeparatorLayer.opacity = 0.0 + self.navigationSeparatorLayerContainer = SimpleLayer() + self.navigationSeparatorLayerContainer.opacity = 0.0 + + self.scrollContainerView = UIView() + self.scrollView = ScrollViewImpl() + + super.init(frame: frame) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + self.addSubview(self.scrollView) + + self.scrollView.addSubview(self.scrollContainerView) + self.scrollContainerView.addSubview(self.transactionsBackground) + + self.addSubview(self.navigationBackgroundView) + + self.navigationSeparatorLayerContainer.addSublayer(self.navigationSeparatorLayer) + self.layer.addSublayer(self.navigationSeparatorLayerContainer) + + self.addSubview(self.headerOffsetContainer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.stateDisposable?.dispose() + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + + if let view = self.chartView.view as? ListItemComponentAdaptor.View, let node = view.itemNode as? StatsGraphItemNode { + node.resetInteraction() + } + } + } + + private var lastScrollBounds: CGRect? + private var lastBottomOffset: CGFloat? + private func updateScrolling(transition: ComponentTransition) { + guard let environment = self.environment?[ViewControllerComponentContainer.Environment.self].value else { + return + } + + let scrollBounds = self.scrollView.bounds + + let topContentOffset = self.scrollView.contentOffset.y + let navigationBackgroundAlpha = min(20.0, max(0.0, topContentOffset)) / 20.0 + + let animatedTransition = ComponentTransition(animation: .curve(duration: 0.18, curve: .easeInOut)) + animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) + animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha) + + let expansionDistance: CGFloat = 32.0 + var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance + expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor)) + + transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor) + + let bottomOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height) + self.lastBottomOffset = bottomOffset + + let transactionsScrollBounds: CGRect + if let transactionsView = self.transactionsView.view { + transactionsScrollBounds = CGRect(origin: CGPoint(x: 0.0, y: scrollBounds.origin.y - transactionsView.frame.minY), size: scrollBounds.size) + } else { + transactionsScrollBounds = .zero + } + self.lastScrollBounds = transactionsScrollBounds + + let _ = self.transactionsView.updateEnvironment( + transition: transition, + environment: { + StarsTransactionsPanelEnvironment( + theme: environment.theme, + strings: environment.strings, + dateTimeFormat: environment.dateTimeFormat, + containerInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right), + isScrollable: false, + isCurrent: true, + externalScrollBounds: transactionsScrollBounds, + externalBottomOffset: bottomOffset + ) + } + ) + } + + private var isUpdating = false + func update(component: StarsStatisticsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + self.component = component + self.environment = environment + self.state = state + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let strings = environment.strings + + if self.stateDisposable == nil { + self.stateDisposable = (component.revenueContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + self.starsState = state.stats + + if !self.isUpdating { + self.state?.updated() + } + }) + } + + self.controller = environment.controller + + self.navigationMetrics = (environment.navigationHeight, environment.statusBarHeight) + + self.navigationSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + + let navigationFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: environment.navigationHeight)) + self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.navigationBackgroundView.update(size: navigationFrame.size, transition: transition.containedViewLayoutTransition) + transition.setFrame(view: self.navigationBackgroundView, frame: navigationFrame) + + let navigationSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel)) + + transition.setFrame(layer: self.navigationSeparatorLayerContainer, frame: navigationSeparatorFrame) + transition.setFrame(layer: self.navigationSeparatorLayer, frame: CGRect(origin: CGPoint(), size: navigationSeparatorFrame.size)) + + self.backgroundColor = environment.theme.list.blocksBackgroundColor + + var contentHeight: CGFloat = 0.0 + + let sideInsets: CGFloat = environment.safeInsets.left + environment.safeInsets.right + 16 * 2.0 + + contentHeight += environment.navigationHeight + contentHeight += 31.0 + + let titleSize = self.titleView.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: strings.Stars_BotRevenue_Title, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + truncationType: .end, + maximumNumberOfLines: 1 + ) + ), + environment: {}, + containerSize: availableSize + ) + if let titleView = self.titleView.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + let titlePosition = CGPoint(x: availableSize.width / 2.0, y: environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0) + transition.setPosition(view: titleView, position: titlePosition) + transition.setBounds(view: titleView, bounds: CGRect(origin: .zero, size: titleSize)) + } + + if let revenueGraph = self.starsState?.revenueGraph { + let chartSize = self.chartView.update( + transition: .immediate, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Stars_BotRevenue_Revenue_Title.uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: StatsGraphItem(presentationData: ItemListPresentationData(presentationData), graph: revenueGraph, type: .stars, noInitialZoom: true, conversionRate: starsState?.usdRate ?? 0.0, sectionId: 0, style: .blocks), + params: ListViewItemLayoutParams(width: availableSize.width - sideInsets, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) + ))), + ], + displaySeparators: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height) + ) + let chartFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - chartSize.width) / 2.0), y: contentHeight), size: chartSize) + if let chartView = self.chartView.view { + if chartView.superview == nil { + self.scrollView.addSubview(chartView) + } + transition.setFrame(view: chartView, frame: chartFrame) + } + contentHeight += chartSize.height + contentHeight += 44.0 + } + + let proceedsSize = self.proceedsView.update( + transition: .immediate, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Stars_BotRevenue_Proceeds_Title.uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Stars_BotRevenue_Proceeds_Info, + font: Font.regular(13.0), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(StarsOverviewItemComponent( + theme: environment.theme, + dateTimeFormat: environment.dateTimeFormat, + title: strings.Stars_BotRevenue_Proceeds_Available, + value: starsState?.balances.availableBalance ?? 0, + rate: starsState?.usdRate ?? 0.0 + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(StarsOverviewItemComponent( + theme: environment.theme, + dateTimeFormat: environment.dateTimeFormat, + title: strings.Stars_BotRevenue_Proceeds_Current, + value: starsState?.balances.currentBalance ?? 0, + rate: starsState?.usdRate ?? 0.0 + ))), + AnyComponentWithIdentity(id: 2, component: AnyComponent(StarsOverviewItemComponent( + theme: environment.theme, + dateTimeFormat: environment.dateTimeFormat, + title: strings.Stars_BotRevenue_Proceeds_Total, + value: starsState?.balances.overallRevenue ?? 0, + rate: starsState?.usdRate ?? 0.0 + ))) + ], + displaySeparators: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height) + ) + let proceedsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - proceedsSize.width) / 2.0), y: contentHeight), size: proceedsSize) + if let proceedsView = self.proceedsView.view { + if proceedsView.superview == nil { + self.scrollView.addSubview(proceedsView) + } + transition.setFrame(view: proceedsView, frame: proceedsFrame) + } + contentHeight += proceedsSize.height + contentHeight += 31.0 + + let termsFont = Font.regular(13.0) + let termsTextColor = environment.theme.list.freeTextColor + let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsFont, textColor: environment.theme.list.itemAccentColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + + let balanceInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(strings.Stars_BotRevenue_Withdraw_Info, attributes: termsMarkdownAttributes, textAlignment: .natural + )) + if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { + self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) + } + if let range = balanceInfoString.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { + balanceInfoString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: balanceInfoString.string)) + } + + let balanceSize = self.balanceView.update( + transition: .immediate, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Stars_BotRevenue_Withdraw_Balance.uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: AnyComponent(MultilineTextComponent( + text: .plain(balanceInfoString), + maximumNumberOfLines: 0, + highlightColor: environment.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: { attributes, _ in + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_BotRevenue_Withdraw_Info_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + } + )), + items: [AnyComponentWithIdentity(id: 0, component: AnyComponent( + StarsBalanceComponent( + theme: environment.theme, + strings: strings, + dateTimeFormat: environment.dateTimeFormat, + count: self.starsState?.balances.availableBalance ?? 0, + rate: self.starsState?.usdRate ?? 0, + actionTitle: strings.Stars_BotRevenue_Withdraw_Withdraw, + actionAvailable: true, + actionIsEnabled: self.starsState?.balances.withdrawEnabled ?? true, + actionCooldownUntilTimestamp: self.starsState?.balances.nextWithdrawalTimestamp, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + var remainingCooldownSeconds: Int32 = 0 + if let cooldownUntilTimestamp = self.starsState?.balances.nextWithdrawalTimestamp { + remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) + remainingCooldownSeconds = max(0, remainingCooldownSeconds) + + if remainingCooldownSeconds > 0 { + component.showTimeoutTooltip(cooldownUntilTimestamp) + } else { + component.withdraw() + } + } else { + component.withdraw() + } + }, + buyAds: { [weak self] in + guard let self, let component = self.component else { + return + } + component.buyAds() + } + ) + ))] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height) + ) + let balanceFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - balanceSize.width) / 2.0), y: contentHeight), size: balanceSize) + if let balanceView = self.balanceView.view { + if balanceView.superview == nil { + self.scrollView.addSubview(balanceView) + } + transition.setFrame(view: balanceView, frame: balanceFrame) + } + + contentHeight += balanceSize.height + contentHeight += 27.0 + + let transactionsHeaderSize = self.transactionsHeader.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Stars_BotRevenue_Transactions_Title.uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: availableSize + ) + let transactionsHeaderFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 32.0, y: contentHeight), size: transactionsHeaderSize) + if let transactionsHeaderView = self.transactionsHeader.view { + if transactionsHeaderView.superview == nil { + self.scrollView.addSubview(transactionsHeaderView) + } + transition.setFrame(view: transactionsHeaderView, frame: transactionsHeaderFrame) + } + contentHeight += transactionsHeaderSize.height + contentHeight += 6.0 + + self.transactionsBackground.backgroundColor = environment.theme.list.itemBlocksBackgroundColor + self.transactionsBackground.layer.cornerRadius = 11.0 + if #available(iOS 13.0, *) { + self.transactionsBackground.layer.cornerCurve = .continuous + } + + let transactionsSize = self.transactionsView.update( + transition: .immediate, + component: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + transactionsContext: component.transactionsContext, + isAccount: false, + action: { transaction in + component.openTransaction(transaction) + } + )), + environment: { + StarsTransactionsPanelEnvironment( + theme: environment.theme, + strings: strings, + dateTimeFormat: environment.dateTimeFormat, + containerInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), + isScrollable: false, + isCurrent: true, + externalScrollBounds: self.lastScrollBounds ?? .zero, + externalBottomOffset: self.lastBottomOffset ?? 1000 + ) + }, + containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height) + ) + self.transactionsView.parentState = state + let transactionsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - transactionsSize.width) / 2.0), y: contentHeight), size: transactionsSize) + if let panelContainerView = self.transactionsView.view { + if panelContainerView.superview == nil { + self.scrollContainerView.addSubview(panelContainerView) + } + transition.setFrame(view: panelContainerView, frame: transactionsFrame) + } + transition.setFrame(view: self.transactionsBackground, frame: transactionsFrame) + + contentHeight += transactionsSize.height + contentHeight += 31.0 + + self.ignoreScrolling = true + + transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center) + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + transition.setFrame(view: self.scrollContainerView, frame: CGRect(origin: CGPoint(), size: contentSize)) + + var scrollViewBounds = self.scrollView.bounds + scrollViewBounds.size = availableSize + transition.setBounds(view: self.scrollView, bounds: scrollViewBounds) + + self.ignoreScrolling = false + + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class StarsStatisticsScreen: ViewControllerComponentContainer { + private let context: AccountContext + private let peerId: EnginePeer.Id + private let revenueContext: StarsRevenueStatsContext + private let transactionsContext: StarsTransactionsContext + + private weak var tooltipScreen: UndoOverlayController? + private var timer: Foundation.Timer? + + public init(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) { + self.context = context + self.peerId = peerId + self.revenueContext = revenueContext + self.transactionsContext = context.engine.payments.peerStarsTransactionsContext(subject: .peer(peerId), mode: .all) + + var withdrawImpl: (() -> Void)? + var buyAdsImpl: (() -> Void)? + var showTimeoutTooltipImpl: ((Int32) -> Void)? + var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)? + super.init(context: context, component: StarsStatisticsScreenComponent( + context: context, + peerId: peerId, + revenueContext: revenueContext, + transactionsContext: self.transactionsContext, + openTransaction: { transaction in + openTransactionImpl?(transaction) + }, + withdraw: { + withdrawImpl?() + }, + showTimeoutTooltip: { timestamp in + showTimeoutTooltipImpl?(timestamp) + }, + buyAds: { + buyAdsImpl?() + } + ), navigationBarAppearance: .transparent) + + self.navigationPresentation = .modalInLargeLayout + + openTransactionImpl = { [weak self] transaction in + guard let self else { + return + } + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + let controller = context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction, peer: peer) + self.push(controller) + }) + } + + withdrawImpl = { [weak self] in + guard let self else { + return + } + + let _ = (context.engine.peers.checkStarsRevenueWithdrawalAvailability() + |> deliverOnMainQueue).start(error: { [weak self] error in + guard let self else { + return + } + switch error { + case .serverProvided: + return + case .requestPassword: + let _ = (revenueContext.state + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] state in + guard let self, let stats = state.stats else { + return + } + let controller = self.context.sharedContext.makeStarsWithdrawalScreen(context: context, stats: stats, completion: { [weak self] amount in + guard let self else { + return + } + let controller = confirmStarsRevenueWithdrawalController(context: context, peerId: peerId, amount: amount, present: { [weak self] c, a in + self?.present(c, in: .window(.root)) + }, completion: { [weak self] url in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + + Queue.mainQueue().after(2.0) { + revenueContext.reload() + self?.transactionsContext.reload() + } + }) + self.present(controller, in: .window(.root)) + }) + self.push(controller) + }) + default: + let controller = starsRevenueWithdrawalController(context: context, peerId: peerId, amount: 0, initialError: error, present: { [weak self] c, a in + self?.present(c, in: .window(.root)) + }, completion: { _ in + + }) + self.present(controller, in: .window(.root)) + } + }) + } + + showTimeoutTooltipImpl = { [weak self] cooldownUntilTimestamp in + guard let self, self.tooltipScreen == nil else { + return + } + + let remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let content: UndoOverlayContent = .universal( + animation: "anim_clock", + scale: 0.058, + colors: [:], + title: nil, + text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorTimeout(stringForRemainingTime(remainingCooldownSeconds)).string, + customUndoText: nil, + timeout: nil + ) + let controller = UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in + return true + }) + self.tooltipScreen = controller + self.present(controller, in: .window(.root)) + + if remainingCooldownSeconds < 3600 { + if self.timer == nil { + self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + + if let tooltipScreen = self.tooltipScreen { + let remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) + let content: UndoOverlayContent = .universal( + animation: "anim_clock", + scale: 0.058, + colors: [:], + title: nil, + text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorTimeout(stringForRemainingTime(remainingCooldownSeconds)).string, + customUndoText: nil, + timeout: nil + ) + tooltipScreen.content = content + } else { + if let timer = self.timer { + self.timer = nil + timer.invalidate() + } + } + }) + } + } + } + + buyAdsImpl = { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let _ = (context.engine.peers.requestStarsRevenueAdsAccountlUrl(peerId: peerId) + |> deliverOnMainQueue).startStandalone(next: { url in + guard let url else { + return + } + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + }) + } + + self.transactionsContext.loadMore() + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index 2efc927a33d..2e8c2b64418 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -15,6 +15,7 @@ import TelegramStringFormatting import AvatarNode import BundleIconComponent import PhotoResources +import StarsAvatarComponent private extension StarsContext.State.Transaction { var extendedId: String { @@ -31,15 +32,18 @@ final class StarsTransactionsListPanelComponent: Component { let context: AccountContext let transactionsContext: StarsTransactionsContext + let isAccount: Bool let action: (StarsContext.State.Transaction) -> Void init( context: AccountContext, transactionsContext: StarsTransactionsContext, + isAccount: Bool, action: @escaping (StarsContext.State.Transaction) -> Void ) { self.context = context self.transactionsContext = transactionsContext + self.isAccount = isAccount self.action = action } @@ -47,6 +51,9 @@ final class StarsTransactionsListPanelComponent: Component { if lhs.context !== rhs.context { return false } + if lhs.isAccount != rhs.isAccount { + return false + } return true } @@ -157,13 +164,14 @@ final class StarsTransactionsListPanelComponent: Component { cancelContextGestures(view: scrollView) } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else { return } - let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -100.0) - + var visibleBounds = environment.externalScrollBounds ?? self.scrollView.bounds + visibleBounds = visibleBounds.insetBy(dx: 0.0, dy: -100.0) + var validIds = Set() if let visibleItems = itemLayout.visibleItems(for: visibleBounds) { for index in visibleItems.lowerBound ..< visibleItems.upperBound { @@ -191,6 +199,7 @@ final class StarsTransactionsListPanelComponent: Component { } separatorView.backgroundColor = environment.theme.list.itemBlocksSeparatorColor + separatorView.isHidden = index == self.items.count - 1 let fontBaseDisplaySize = 17.0 @@ -199,7 +208,10 @@ final class StarsTransactionsListPanelComponent: Component { var itemDate: String switch item.peer { case let .peer(peer): - if let title = item.title { + if !item.media.isEmpty { + itemTitle = environment.strings.Stars_Intro_Transaction_MediaPurchase + itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) + } else if let title = item.title { itemTitle = title itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) } else { @@ -213,11 +225,19 @@ final class StarsTransactionsListPanelComponent: Component { itemTitle = environment.strings.Stars_Intro_Transaction_GoogleTopUp_Title itemSubtitle = environment.strings.Stars_Intro_Transaction_GoogleTopUp_Subtitle case .fragment: - itemTitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Title - itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Subtitle + if component.isAccount { + itemTitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Title + itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Subtitle + } else { + itemTitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Title + itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Subtitle + } case .premiumBot: itemTitle = environment.strings.Stars_Intro_Transaction_PremiumBotTopUp_Title itemSubtitle = environment.strings.Stars_Intro_Transaction_PremiumBotTopUp_Subtitle + case .ads: + itemTitle = environment.strings.Stars_Intro_Transaction_TelegramAds_Title + itemSubtitle = environment.strings.Stars_Intro_Transaction_TelegramAds_Subtitle case .unsupported: itemTitle = environment.strings.Stars_Intro_Transaction_Unsupported_Title itemSubtitle = nil @@ -278,9 +298,9 @@ final class StarsTransactionsListPanelComponent: Component { theme: environment.theme, title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)), contentInsets: UIEdgeInsets(top: 9.0, left: environment.containerInsets.left, bottom: 8.0, right: environment.containerInsets.right), - leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(AvatarComponent(context: component.context, theme: environment.theme, peer: item.peer, photo: item.photo))), false), + leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: item.peer, photo: item.photo, media: item.media, backgroundColor: environment.theme.list.plainBackgroundColor))), false), icon: nil, - accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(LabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), + accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), action: { [weak self] _ in guard let self, let component = self.component else { return @@ -330,7 +350,7 @@ final class StarsTransactionsListPanelComponent: Component { self.visibleItems.removeValue(forKey: id) } - let bottomOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height) + let bottomOffset = self.environment?.externalBottomOffset ?? max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height) let loadMore = bottomOffset < 100.0 if environment.isCurrent, loadMore { let lastId = self.items.last?.extendedId @@ -342,7 +362,7 @@ final class StarsTransactionsListPanelComponent: Component { } private var isUpdating = false - func update(component: StarsTransactionsListPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StarsTransactionsListPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -424,11 +444,18 @@ final class StarsTransactionsListPanelComponent: Component { self.ignoreScrolling = true let contentOffset = self.scrollView.bounds.minY - transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center) + var scrollBounds = self.scrollView.bounds - scrollBounds.size = availableSize - if !environment.isScrollable { + if let _ = environment.externalScrollBounds { scrollBounds.origin = CGPoint() + scrollBounds.size = CGSize(width: availableSize.width, height: itemLayout.contentHeight) + transition.setPosition(view: self.scrollView, position: scrollBounds.center) + } else { + transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center) + scrollBounds.size = availableSize + if !environment.isScrollable { + scrollBounds.origin = CGPoint() + } } transition.setBounds(view: self.scrollView, bounds: scrollBounds) self.scrollView.isScrollEnabled = environment.isScrollable @@ -444,7 +471,11 @@ final class StarsTransactionsListPanelComponent: Component { self.ignoreScrolling = false self.updateScrolling(transition: transition) - return availableSize + if let _ = environment.externalScrollBounds { + return CGSize(width: availableSize.width, height: contentSize.height) + } else { + return availableSize + } } } @@ -452,7 +483,7 @@ final class StarsTransactionsListPanelComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -469,241 +500,3 @@ func cancelContextGestures(view: UIView) { cancelContextGestures(view: subview) } } - -private final class AvatarComponent: Component { - let context: AccountContext - let theme: PresentationTheme - let peer: StarsContext.State.Transaction.Peer - let photo: TelegramMediaWebFile? - - init(context: AccountContext, theme: PresentationTheme, peer: StarsContext.State.Transaction.Peer, photo: TelegramMediaWebFile?) { - self.context = context - self.theme = theme - self.peer = peer - self.photo = photo - } - - static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.peer != rhs.peer { - return false - } - if lhs.photo != rhs.photo { - return false - } - return true - } - - final class View: UIView { - private let avatarNode: AvatarNode - private let backgroundView = UIImageView() - private let iconView = UIImageView() - private var imageNode: TransformImageNode? - - private let fetchDisposable = MetaDisposable() - - private var component: AvatarComponent? - private weak var state: EmptyComponentState? - - override init(frame: CGRect) { - self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0)) - - super.init(frame: frame) - - self.iconView.contentMode = .scaleAspectFit - - self.addSubnode(self.avatarNode) - self.addSubview(self.backgroundView) - self.addSubview(self.iconView) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.fetchDisposable.dispose() - } - - func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - self.component = component - self.state = state - - let size = CGSize(width: 40.0, height: 40.0) - var iconInset: CGFloat = 3.0 - var iconOffset: CGFloat = 0.0 - - switch component.peer { - case let .peer(peer): - if let photo = component.photo { - let imageNode: TransformImageNode - if let current = self.imageNode { - imageNode = current - } else { - imageNode = TransformImageNode() - imageNode.contentAnimations = [.subsequentUpdates] - self.addSubview(imageNode.view) - self.imageNode = imageNode - - imageNode.setSignal(chatWebFileImage(account: component.context.account, file: photo)) - self.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: component.context.account, userLocation: .other, image: photo).startStrict()) - } - - imageNode.frame = CGRect(origin: .zero, size: size) - imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: size.width / 2.0), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() - - self.backgroundView.isHidden = true - self.iconView.isHidden = true - self.avatarNode.isHidden = true - } else { - self.avatarNode.setPeer( - context: component.context, - theme: component.theme, - peer: peer, - synchronousLoad: true - ) - self.backgroundView.isHidden = true - self.iconView.isHidden = true - self.avatarNode.isHidden = false - } - case .appStore: - self.backgroundView.image = generateGradientFilledCircleImage( - diameter: size.width, - colors: [ - UIColor(rgb: 0x2a9ef1).cgColor, - UIColor(rgb: 0x72d5fd).cgColor - ], - direction: .mirroredDiagonal - ) - self.backgroundView.isHidden = false - self.iconView.isHidden = false - self.avatarNode.isHidden = true - self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Apple") - case .playMarket: - self.backgroundView.image = generateGradientFilledCircleImage( - diameter: size.width, - colors: [ - UIColor(rgb: 0x54cb68).cgColor, - UIColor(rgb: 0xa0de7e).cgColor - ], - direction: .mirroredDiagonal - ) - self.backgroundView.isHidden = false - self.iconView.isHidden = false - self.avatarNode.isHidden = true - self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Google") - case .fragment: - self.backgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24)) - self.backgroundView.isHidden = false - self.iconView.isHidden = false - self.avatarNode.isHidden = true - self.iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment") - iconOffset = 2.0 - case .premiumBot: - iconInset = 7.0 - self.backgroundView.image = generateGradientFilledCircleImage( - diameter: size.width, - colors: [ - UIColor(rgb: 0x6b93ff).cgColor, - UIColor(rgb: 0x6b93ff).cgColor, - UIColor(rgb: 0x8d77ff).cgColor, - UIColor(rgb: 0xb56eec).cgColor, - UIColor(rgb: 0xb56eec).cgColor - ], - direction: .mirroredDiagonal - ) - self.backgroundView.isHidden = false - self.iconView.isHidden = false - self.avatarNode.isHidden = true - self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white) - case .unsupported: - iconInset = 7.0 - self.backgroundView.image = generateGradientFilledCircleImage( - diameter: size.width, - colors: [ - UIColor(rgb: 0xb1b1b1).cgColor, - UIColor(rgb: 0xcdcdcd).cgColor - ], - direction: .mirroredDiagonal - ) - self.backgroundView.isHidden = false - self.iconView.isHidden = false - self.avatarNode.isHidden = true - self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white) - } - - self.avatarNode.frame = CGRect(origin: .zero, size: size) - self.iconView.frame = CGRect(origin: .zero, size: size).insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset) - self.backgroundView.frame = CGRect(origin: .zero, size: size) - - return size - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - -private final class LabelComponent: CombinedComponent { - let text: NSAttributedString - - init( - text: NSAttributedString - ) { - self.text = text - } - - static func ==(lhs: LabelComponent, rhs: LabelComponent) -> Bool { - if lhs.text != rhs.text { - return false - } - return true - } - - static var body: Body { - let text = Child(MultilineTextComponent.self) - let icon = Child(BundleIconComponent.self) - - return { context in - let component = context.component - - let text = text.update( - component: MultilineTextComponent(text: .plain(component.text)), - availableSize: CGSize(width: 100.0, height: 40.0), - transition: context.transition - ) - - let iconSize = CGSize(width: 20.0, height: 20.0) - let icon = icon.update( - component: BundleIconComponent( - name: "Premium/Stars/StarLarge", - tintColor: nil - ), - availableSize: iconSize, - transition: context.transition - ) - - let spacing: CGFloat = 3.0 - let totalWidth = text.size.width + spacing + iconSize.width - let size = CGSize(width: totalWidth, height: iconSize.height) - - context.add(text - .position(CGPoint(x: text.size.width / 2.0, y: size.height / 2.0)) - ) - context.add(icon - .position(CGPoint(x: totalWidth - iconSize.width / 2.0, y: size.height / 2.0 - UIScreenPixel)) - ) - return size - } - } -} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsPanelContainerComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsPanelContainerComponent.swift index 2311dd02f2a..2b1e6896ccd 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsPanelContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsPanelContainerComponent.swift @@ -29,6 +29,8 @@ final class StarsTransactionsPanelEnvironment: Equatable { let containerInsets: UIEdgeInsets let isScrollable: Bool let isCurrent: Bool + let externalScrollBounds: CGRect? + let externalBottomOffset: CGFloat? init( theme: PresentationTheme, @@ -36,7 +38,9 @@ final class StarsTransactionsPanelEnvironment: Equatable { dateTimeFormat: PresentationDateTimeFormat, containerInsets: UIEdgeInsets, isScrollable: Bool, - isCurrent: Bool + isCurrent: Bool, + externalScrollBounds: CGRect? = nil, + externalBottomOffset: CGFloat? = nil ) { self.theme = theme self.strings = strings @@ -44,6 +48,8 @@ final class StarsTransactionsPanelEnvironment: Equatable { self.containerInsets = containerInsets self.isScrollable = isScrollable self.isCurrent = isCurrent + self.externalScrollBounds = externalScrollBounds + self.externalBottomOffset = externalBottomOffset } static func ==(lhs: StarsTransactionsPanelEnvironment, rhs: StarsTransactionsPanelEnvironment) -> Bool { @@ -65,6 +71,12 @@ final class StarsTransactionsPanelEnvironment: Equatable { if lhs.isCurrent != rhs.isCurrent { return false } + if lhs.externalScrollBounds != rhs.externalScrollBounds { + return false + } + if lhs.externalBottomOffset != rhs.externalBottomOffset { + return false + } return true } } @@ -249,7 +261,7 @@ private final class StarsTransactionsHeaderComponent: Component { } } - func update(component: StarsTransactionsHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StarsTransactionsHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme self.component = component @@ -348,7 +360,7 @@ private final class StarsTransactionsHeaderComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -377,7 +389,7 @@ final class StarsTransactionsPanelContainerComponent: Component { let dateTimeFormat: PresentationDateTimeFormat let insets: UIEdgeInsets let items: [Item] - let currentPanelUpdated: (AnyHashable, Transition) -> Void + let currentPanelUpdated: (AnyHashable, ComponentTransition) -> Void init( theme: PresentationTheme, @@ -385,7 +397,7 @@ final class StarsTransactionsPanelContainerComponent: Component { dateTimeFormat: PresentationDateTimeFormat, insets: UIEdgeInsets, items: [Item], - currentPanelUpdated: @escaping (AnyHashable, Transition) -> Void + currentPanelUpdated: @escaping (AnyHashable, ComponentTransition) -> Void ) { self.theme = theme self.strings = strings @@ -561,7 +573,7 @@ final class StarsTransactionsPanelContainerComponent: Component { } self.transitionFraction = 0.0 - let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring)) if let currentId = self.currentId { self.state?.updated(transition: transition) component.currentPanelUpdated(currentId, transition) @@ -576,12 +588,12 @@ final class StarsTransactionsPanelContainerComponent: Component { } } - func updateNavigationMergeFactor(value: CGFloat, transition: Transition) { + func updateNavigationMergeFactor(value: CGFloat, transition: ComponentTransition) { transition.setAlpha(view: self.topPanelMergedBackgroundView, alpha: value) transition.setAlpha(view: self.topPanelBackgroundView, alpha: 1.0 - value) } - func update(component: StarsTransactionsPanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StarsTransactionsPanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[StarsTransactionsPanelContainerEnvironment.self].value let themeUpdated = self.component?.theme !== component.theme @@ -649,7 +661,7 @@ final class StarsTransactionsPanelContainerComponent: Component { } if component.items.contains(where: { $0.id == id }) { self.currentId = id - let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring)) self.state?.updated(transition: transition) component.currentPanelUpdated(id, transition) } @@ -799,7 +811,7 @@ final class StarsTransactionsPanelContainerComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 2659ac2f523..cd66fc13902 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -78,10 +78,7 @@ final class StarsTransactionsScreenComponent: Component { private let navigationBackgroundView: BlurredBackgroundView private let navigationSeparatorLayer: SimpleLayer private let navigationSeparatorLayerContainer: SimpleLayer - - private let headerView = ComponentView() - private let headerOffsetContainer: UIView - + private let scrollContainerView: UIView private let overscroll = ComponentView() @@ -119,9 +116,6 @@ final class StarsTransactionsScreenComponent: Component { private var outgoingTransactionsContext: StarsTransactionsContext? override init(frame: CGRect) { - self.headerOffsetContainer = UIView() - self.headerOffsetContainer.isUserInteractionEnabled = false - self.navigationBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true) self.navigationBackgroundView.alpha = 0.0 @@ -158,8 +152,6 @@ final class StarsTransactionsScreenComponent: Component { self.navigationSeparatorLayerContainer.addSublayer(self.navigationSeparatorLayer) self.layer.addSublayer(self.navigationSeparatorLayerContainer) - - self.addSubview(self.headerOffsetContainer) } required init?(coder: NSCoder) { @@ -202,7 +194,7 @@ final class StarsTransactionsScreenComponent: Component { } } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { let scrollBounds = self.scrollView.bounds let isLockedAtPanels = scrollBounds.maxY == self.scrollView.contentSize.height @@ -222,7 +214,7 @@ final class StarsTransactionsScreenComponent: Component { let fraction = max(0.0, min(1.0, titleOffset / titleOffsetDelta)) titleScale = 1.0 - fraction * 0.36 - let headerTransition: Transition = .immediate + let headerTransition: ComponentTransition = .immediate if let starView = self.starView.view { let starPosition = CGPoint(x: self.scrollView.frame.width / 2.0, y: topInset + starView.bounds.height / 2.0 - 30.0 - titleOffset * titleScale) @@ -238,7 +230,7 @@ final class StarsTransactionsScreenComponent: Component { headerTransition.setScale(view: titleView, scale: titleScale) } - let animatedTransition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut)) + let animatedTransition = ComponentTransition(animation: .curve(duration: 0.18, curve: .easeInOut)) animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha) @@ -272,7 +264,7 @@ final class StarsTransactionsScreenComponent: Component { } private var isUpdating = false - func update(component: StarsTransactionsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StarsTransactionsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -283,7 +275,7 @@ final class StarsTransactionsScreenComponent: Component { var balanceUpdated = false if let starsState = self.starsState { - if let previousBalance, starsState.balance != previousBalance { + if let previousBalance = self.previousBalance, starsState.balance != previousBalance { balanceUpdated = true } self.previousBalance = starsState.balance @@ -337,7 +329,7 @@ final class StarsTransactionsScreenComponent: Component { contentHeight += environment.statusBarHeight - let starTransition: Transition = .immediate + let starTransition: ComponentTransition = .immediate var topBackgroundColor = environment.theme.list.plainBackgroundColor let bottomBackgroundColor = environment.theme.list.blocksBackgroundColor @@ -529,13 +521,17 @@ final class StarsTransactionsScreenComponent: Component { strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, count: self.starsState?.balance ?? 0, - purchaseAvailable: !premiumConfiguration.areStarsDisabled, - buy: { [weak self] in + rate: nil, + actionTitle: environment.strings.Stars_Intro_Buy, + actionAvailable: !premiumConfiguration.areStarsDisabled, + actionIsEnabled: true, + action: { [weak self] in guard let self, let component = self.component else { return } component.buy() - } + }, + buyAds: nil ) ))] )), @@ -560,21 +556,24 @@ final class StarsTransactionsScreenComponent: Component { if let current = self.allTransactionsContext { allTransactionsContext = current } else { - allTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(starsContext: component.starsContext, subject: .all) + allTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .starsContext(component.starsContext), mode: .all) + self.allTransactionsContext = allTransactionsContext } let incomingTransactionsContext: StarsTransactionsContext if let current = self.incomingTransactionsContext { incomingTransactionsContext = current } else { - incomingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(starsContext: component.starsContext, subject: .incoming) + incomingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .starsContext(component.starsContext), mode: .incoming) + self.incomingTransactionsContext = incomingTransactionsContext } let outgoingTransactionsContext: StarsTransactionsContext if let current = self.outgoingTransactionsContext { outgoingTransactionsContext = current } else { - outgoingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(starsContext: component.starsContext, subject: .outgoing) + outgoingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .starsContext(component.starsContext), mode: .outgoing) + self.outgoingTransactionsContext = outgoingTransactionsContext } panelItems.append(StarsTransactionsPanelContainerComponent.Item( @@ -583,6 +582,7 @@ final class StarsTransactionsScreenComponent: Component { panel: AnyComponent(StarsTransactionsListPanelComponent( context: component.context, transactionsContext: allTransactionsContext, + isAccount: true, action: { transaction in component.openTransaction(transaction) } @@ -595,6 +595,7 @@ final class StarsTransactionsScreenComponent: Component { panel: AnyComponent(StarsTransactionsListPanelComponent( context: component.context, transactionsContext: incomingTransactionsContext, + isAccount: true, action: { transaction in component.openTransaction(transaction) } @@ -607,6 +608,7 @@ final class StarsTransactionsScreenComponent: Component { panel: AnyComponent(StarsTransactionsListPanelComponent( context: component.context, transactionsContext: outgoingTransactionsContext, + isAccount: true, action: { transaction in component.openTransaction(transaction) } @@ -686,7 +688,7 @@ final class StarsTransactionsScreenComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -714,14 +716,22 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { } ), navigationBarAppearance: .transparent) + self.navigationPresentation = .modalInLargeLayout + self.options.set(.single([]) |> then(context.engine.payments.starsTopUpOptions())) openTransactionImpl = { [weak self] transaction in guard let self else { return } - let controller = context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction) - self.push(controller) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + let controller = context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction, peer: peer) + self.push(controller) + }) } buyImpl = { [weak self] in diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsUtils.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsUtils.swift new file mode 100644 index 00000000000..f09a7ed3ce6 --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsUtils.swift @@ -0,0 +1,6 @@ +import Foundation + +func formatUsdValue(_ value: Int64, rate: Double) -> String { + let formattedValue = String(format: "%0.2f", (Double(value)) * rate) + return "$\(formattedValue)" +} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 3d45440c82e..632e45c61f7 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -26,6 +26,7 @@ private final class SheetContent: CombinedComponent { let starsContext: StarsContext let invoice: TelegramMediaInvoice let source: BotPaymentInvoiceSource + let extendedMedia: [TelegramExtendedMedia] let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> let dismiss: () -> Void @@ -34,6 +35,7 @@ private final class SheetContent: CombinedComponent { starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, + extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, dismiss: @escaping () -> Void ) { @@ -41,6 +43,7 @@ private final class SheetContent: CombinedComponent { self.starsContext = starsContext self.invoice = invoice self.source = source + self.extendedMedia = extendedMedia self.inputData = inputData self.dismiss = dismiss } @@ -52,6 +55,9 @@ private final class SheetContent: CombinedComponent { if lhs.invoice != rhs.invoice { return false } + if lhs.extendedMedia != rhs.extendedMedia { + return false + } return true } @@ -62,9 +68,11 @@ private final class SheetContent: CombinedComponent { private let context: AccountContext private let starsContext: StarsContext private let source: BotPaymentInvoiceSource + private let extendedMedia: [TelegramExtendedMedia] private let invoice: TelegramMediaInvoice - private(set) var peer: EnginePeer? + private(set) var botPeer: EnginePeer? + private(set) var chatPeer: EnginePeer? private var peerDisposable: Disposable? private(set) var balance: Int64? private(set) var form: BotPaymentForm? @@ -85,24 +93,37 @@ private final class SheetContent: CombinedComponent { context: AccountContext, starsContext: StarsContext, source: BotPaymentInvoiceSource, + extendedMedia: [TelegramExtendedMedia], invoice: TelegramMediaInvoice, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> ) { self.context = context self.starsContext = starsContext self.source = source + self.extendedMedia = extendedMedia self.invoice = invoice super.init() - self.peerDisposable = (inputData - |> deliverOnMainQueue).start(next: { [weak self] inputData in + let chatPeer: Signal + if case let .message(messageId) = source { + chatPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)) + } else { + chatPeer = .single(nil) + } + + self.peerDisposable = (combineLatest( + inputData, + chatPeer + ) + |> deliverOnMainQueue).start(next: { [weak self] inputData, chatPeer in guard let self else { return } self.balance = inputData?.0.balance ?? 0 self.form = inputData?.1 - self.peer = inputData?.2 + self.botPeer = inputData?.2 + self.chatPeer = chatPeer self.updated(transition: .immediate) if self.optionsDisposable == nil, let balance = self.balance, balance < self.invoice.totalAmount { @@ -132,7 +153,7 @@ private final class SheetContent: CombinedComponent { self.optionsDisposable?.dispose() } - func buy(requestTopUp: @escaping (@escaping () -> Void) -> Void, completion: @escaping () -> Void) { + func buy(requestTopUp: @escaping (@escaping () -> Void) -> Void, completion: @escaping (Bool) -> Void) { guard let form, let balance else { return } @@ -146,7 +167,20 @@ private final class SheetContent: CombinedComponent { let _ = (self.context.engine.payments.sendStarsPaymentForm(formId: form.id, source: self.source) |> deliverOnMainQueue).start(next: { _ in - completion() + completion(true) + }, error: { [weak self] error in + guard let self else { + return + } + switch error { + case .alreadyPaid: + if !self.extendedMedia.isEmpty, case let .message(messageId) = self.source { + let _ = self.context.engine.messages.updateExtendedMedia(messageIds: [messageId]).startStandalone() + } + default: + break + } + completion(false) }) } @@ -179,7 +213,16 @@ private final class SheetContent: CombinedComponent { } |> take(1) |> deliverOnMainQueue).start(next: { _ in - action() + Queue.mainQueue().after(0.1, { [weak self] in + if let self, let balance = self.balance, balance < self.invoice.totalAmount { + self.inProgress = false + self.updated() + + self.buy(requestTopUp: requestTopUp, completion: completion) + } else { + action() + } + }) }) }) } @@ -191,7 +234,7 @@ private final class SheetContent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, starsContext: self.starsContext, source: self.source, invoice: self.invoice, inputData: self.inputData) + return State(context: self.context, starsContext: self.starsContext, source: self.source, extendedMedia: self.extendedMedia, invoice: self.invoice, inputData: self.inputData) } static var body: Body { @@ -227,28 +270,32 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) ) - if let peer = state.peer { - let subject: StarsImageComponent.Subject + let subject: StarsImageComponent.Subject + if !component.extendedMedia.isEmpty { + subject = .extendedMedia(component.extendedMedia) + } else if let peer = state.botPeer { if let photo = component.invoice.photo { subject = .photo(photo) } else { subject = .transactionPeer(.peer(peer)) } - let star = star.update( - component: StarsImageComponent( - context: component.context, - subject: subject, - theme: theme, - diameter: 90.0 - ), - availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), - transition: context.transition - ) - - context.add(star - .position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 - 27.0)) - ) + } else { + subject = .none } + let star = star.update( + component: StarsImageComponent( + context: component.context, + subject: subject, + theme: theme, + diameter: 90.0, + backgroundColor: theme.list.blocksBackgroundColor + ), + availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), + transition: context.transition + ) + context.add(star + .position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 - 27.0)) + ) let closeImage: UIImage if let (image, cacheTheme) = state.cachedCloseImage, theme === cacheTheme { @@ -295,14 +342,50 @@ private final class SheetContent: CombinedComponent { }) let amount = component.invoice.totalAmount + let infoText: String + if !component.extendedMedia.isEmpty { + var description: String = "" + var photoCount: Int32 = 0 + var videoCount: Int32 = 0 + for media in component.extendedMedia { + if case let .preview(_, _, videoDuration) = media, videoDuration != nil { + videoCount += 1 + } else { + photoCount += 1 + } + } + if photoCount > 0 && videoCount > 0 { + description = strings.Stars_Transfer_MediaAnd("**\(strings.Stars_Transfer_Photos(photoCount))**", "**\(strings.Stars_Transfer_Videos(videoCount))**").string + } else if photoCount > 0 { + if photoCount > 1 { + description += "**\(strings.Stars_Transfer_Photos(photoCount))**" + } else { + description += "**\(strings.Stars_Transfer_SinglePhoto)**" + } + } else if videoCount > 0 { + if videoCount > 1 { + description += "**\(strings.Stars_Transfer_Videos(videoCount))**" + } else { + description += "**\(strings.Stars_Transfer_SingleVideo)**" + } + } + infoText = strings.Stars_Transfer_UnlockInfo( + description, + state.chatPeer?.compactDisplayTitle ?? "", + strings.Stars_Transfer_Info_Stars(Int32(amount)) + ).string + } else { + infoText = strings.Stars_Transfer_Info( + component.invoice.title, + state.botPeer?.compactDisplayTitle ?? "", + strings.Stars_Transfer_Info_Stars(Int32(amount)) + ).string + } + let text = text.update( component: BalancedTextComponent( text: .markdown( - text: strings.Stars_Transfer_Info( - component.invoice.title, - state.peer?.compactDisplayTitle ?? "", - strings.Stars_Transfer_Info_Stars(Int32(amount)) - ).string, + text: infoText, attributes: markdownAttributes ), horizontalAlignment: .center, @@ -363,7 +446,8 @@ private final class SheetContent: CombinedComponent { state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, theme) } - let buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amount)", font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) + let amountString = presentationStringsFormattedNumber(Int32(amount), presentationData.dateTimeFormat.groupingSeparator) + let buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amountString)", font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) if let range = buttonAttributedString.string.range(of: "#"), let starImage = state.cachedStarImage?.0 { buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string)) @@ -374,8 +458,9 @@ private final class SheetContent: CombinedComponent { let accountContext = component.context let starsContext = component.starsContext - let botTitle = state.peer?.compactDisplayTitle ?? "" + let botTitle = state.botPeer?.compactDisplayTitle ?? "" let invoice = component.invoice + let isMedia = !component.extendedMedia.isEmpty let button = button.update( component: ButtonComponent( background: ButtonComponent.Background( @@ -398,7 +483,7 @@ private final class SheetContent: CombinedComponent { context: accountContext, starsContext: starsContext, options: state?.options ?? [], - peerId: state?.peer?.id, + peerId: isMedia ? nil : state?.botPeer?.id, requiredStars: invoice.totalAmount, completion: { [weak starsContext] stars in starsContext?.add(balance: stars) @@ -412,29 +497,38 @@ private final class SheetContent: CombinedComponent { let alertController = textAlertController(context: accountContext, title: nil, text: presentationData.strings.Stars_Transfer_Unavailable, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) controller?.present(alertController, in: .window(.root)) } - }, completion: { [weak controller] in - let presentationData = accountContext.sharedContext.currentPresentationData.with { $0 } - if let navigationController = controller?.navigationController { - Queue.mainQueue().after(0.5) { - if let lastController = navigationController.viewControllers.last as? ViewController { - let resultController = UndoOverlayController( - presentationData: presentationData, - content: .image( - image: UIImage(bundleImageName: "Premium/Stars/StarLarge")!, - title: presentationData.strings.Stars_Transfer_PurchasedTitle, - text: presentationData.strings.Stars_Transfer_PurchasedText(invoice.title, botTitle, presentationData.strings.Stars_Transfer_Purchased_Stars(Int32(invoice.totalAmount))).string, - round: false, - undoText: nil - ), - elevatedLayout: lastController is ChatController, - action: { _ in return true} - ) - lastController.present(resultController, in: .window(.root)) + }, completion: { [weak controller] success in + if success { + let presentationData = accountContext.sharedContext.currentPresentationData.with { $0 } + let text: String + if let _ = component.invoice.extendedMedia { + text = presentationData.strings.Stars_Transfer_UnlockedText( presentationData.strings.Stars_Transfer_Purchased_Stars(Int32(invoice.totalAmount))).string + } else { + text = presentationData.strings.Stars_Transfer_PurchasedText(invoice.title, botTitle, presentationData.strings.Stars_Transfer_Purchased_Stars(Int32(invoice.totalAmount))).string + } + + if let navigationController = controller?.navigationController { + Queue.mainQueue().after(0.5) { + if let lastController = navigationController.viewControllers.last as? ViewController { + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .image( + image: UIImage(bundleImageName: "Premium/Stars/StarLarge")!, + title: presentationData.strings.Stars_Transfer_PurchasedTitle, + text: text, + round: false, + undoText: nil + ), + elevatedLayout: lastController is ChatController, + action: { _ in return true} + ) + lastController.present(resultController, in: .window(.root)) + } } } } - controller?.complete(paid: true) + controller?.complete(paid: success) controller?.dismissAnimated() starsContext.load(force: true) @@ -464,6 +558,7 @@ private final class StarsTransferSheetComponent: CombinedComponent { private let starsContext: StarsContext private let invoice: TelegramMediaInvoice private let source: BotPaymentInvoiceSource + private let extendedMedia: [TelegramExtendedMedia] private let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> init( @@ -471,12 +566,14 @@ private final class StarsTransferSheetComponent: CombinedComponent { starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, + extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> ) { self.context = context self.starsContext = starsContext self.invoice = invoice self.source = source + self.extendedMedia = extendedMedia self.inputData = inputData } @@ -487,6 +584,9 @@ private final class StarsTransferSheetComponent: CombinedComponent { if lhs.invoice != rhs.invoice { return false } + if lhs.extendedMedia != rhs.extendedMedia { + return false + } return true } @@ -506,6 +606,7 @@ private final class StarsTransferSheetComponent: CombinedComponent { starsContext: context.component.starsContext, invoice: context.component.invoice, source: context.component.source, + extendedMedia: context.component.extendedMedia, inputData: context.component.inputData, dismiss: { animateOut.invoke(Action { _ in @@ -564,6 +665,7 @@ public final class StarsTransferScreen: ViewControllerComponentContainer { starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, + extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void ) { @@ -577,6 +679,7 @@ public final class StarsTransferScreen: ViewControllerComponentContainer { starsContext: starsContext, invoice: invoice, source: source, + extendedMedia: extendedMedia, inputData: inputData ), navigationBarAppearance: .none, diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD new file mode 100644 index 00000000000..05069f6b5c0 --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD @@ -0,0 +1,44 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "StarsWithdrawalScreen", + module_name = "StarsWithdrawalScreen", + 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/TextFormat", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/ScrollComponent", + "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/PasswordSetupUI", + "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsRevenueWithdrawalController.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsRevenueWithdrawalController.swift new file mode 100644 index 00000000000..e56d23f8633 --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsRevenueWithdrawalController.swift @@ -0,0 +1,112 @@ +import Foundation +import Display +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import PresentationDataUtils +import AccountContext +import PasswordSetupUI +import Markdown +import OwnershipTransferController + +public func confirmStarsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, amount: Int64, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController { + let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + + var dismissImpl: (() -> Void)? + var proceedImpl: (() -> Void)? + + let disposable = MetaDisposable() + + let contentNode = ChannelOwnershipTransferAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, title: presentationData.strings.Monetization_Withdraw_EnterPassword_Title, text: presentationData.strings.Monetization_Withdraw_EnterPassword_Text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?() + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Monetization_Withdraw_EnterPassword_Done, action: { + proceedImpl?() + })]) + + contentNode.complete = { + proceedImpl?() + } + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in + controller?.theme = AlertControllerTheme(presentationData: presentationData) + contentNode?.theme = presentationData.theme + }) + controller.dismissed = { _ in + presentationDataDisposable.dispose() + disposable.dispose() + } + dismissImpl = { [weak controller, weak contentNode] in + contentNode?.dismissInput() + controller?.dismissAnimated() + } + proceedImpl = { [weak contentNode] in + guard let contentNode = contentNode else { + return + } + contentNode.updateIsChecking(true) + + let signal = context.engine.peers.requestStarsRevenueWithdrawalUrl(peerId: peerId, amount: amount, password: contentNode.password) + disposable.set((signal |> deliverOnMainQueue).start(next: { url in + dismissImpl?() + completion(url) + }, error: { [weak contentNode] error in + var errorTextAndActions: (String, [TextAlertAction])? + switch error { + case .invalidPassword: + contentNode?.animateError() + case .limitExceeded: + errorTextAndActions = (presentationData.strings.TwoStepAuth_FloodError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + default: + errorTextAndActions = (presentationData.strings.Login_UnknownError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + } + contentNode?.updateIsChecking(false) + + if let (text, actions) = errorTextAndActions { + dismissImpl?() + present(textAlertController(context: context, title: nil, text: text, actions: actions), nil) + } + })) + } + + return controller +} + + +public func starsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, amount: Int64, initialError: RequestStarsRevenueWithdrawalError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController { + let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + let theme = AlertControllerTheme(presentationData: presentationData) + + var title: NSAttributedString? = NSAttributedString(string: presentationData.strings.OwnershipTransfer_SecurityCheck, font: Font.semibold(presentationData.listsFontSize.itemListBaseFontSize), textColor: theme.primaryColor, paragraphAlignment: .center) + + var text = presentationData.strings.Monetization_Withdraw_SecurityRequirements + let textFontSize = presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0 + + var actions: [TextAlertAction] = [] + switch initialError { + case .requestPassword: + return confirmStarsRevenueWithdrawalController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, amount: amount, present: present, completion: completion) + case .twoStepAuthTooFresh, .authSessionTooFresh: + text = text + presentationData.strings.Monetization_Withdraw_ComeBackLater + actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] + case .twoStepAuthMissing: + actions = [TextAlertAction(type: .genericAction, title: presentationData.strings.OwnershipTransfer_SetupTwoStepAuth, action: { + let controller = SetupTwoStepVerificationController(context: context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in + if shouldDismiss { + controller.dismiss() + } + }) + present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})] + default: + title = nil + text = presentationData.strings.Login_UnknownError + actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] + } + + let body = MarkdownAttributeSet(font: Font.regular(textFontSize), textColor: theme.primaryColor) + let bold = MarkdownAttributeSet(font: Font.semibold(textFontSize), textColor: theme.primaryColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) + + return richTextAlertController(context: context, title: title, text: attributedText, actions: actions) +} diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift new file mode 100644 index 00000000000..436374d47e0 --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -0,0 +1,835 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import ViewControllerComponent +import SheetComponent +import BalancedTextComponent +import MultilineTextComponent +import BundleIconComponent +import ButtonComponent +import ItemListUI +import AccountContext +import PresentationDataUtils +import ListSectionComponent +import TelegramStringFormatting +import UndoUI + +private let amountTag = GenericComponentViewTag() + +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let mode: StarsWithdrawScreen.Mode + let dismiss: () -> Void + + init( + context: AccountContext, + mode: StarsWithdrawScreen.Mode, + dismiss: @escaping () -> Void + ) { + self.context = context + self.mode = mode + self.dismiss = dismiss + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.mode != rhs.mode { + return false + } + return true + } + + static var body: Body { + let background = Child(RoundedRectangle.self) + let closeButton = Child(Button.self) + let title = Child(Text.self) + let urlSection = Child(ListSectionComponent.self) + let button = Child(ButtonComponent.self) + let balanceTitle = Child(MultilineTextComponent.self) + let balanceValue = Child(MultilineTextComponent.self) + let balanceIcon = Child(BundleIconComponent.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let component = context.component + let state = context.state + + let theme = environment.theme.withModalBlocksBackground() + let strings = environment.strings + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let sideInset: CGFloat = 16.0 + var contentSize = CGSize(width: context.availableSize.width, height: 18.0) + + let background = background.update( + component: RoundedRectangle(color: theme.list.blocksBackgroundColor, 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 constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 + + let closeImage: UIImage + if let (image, theme) = state.cachedCloseImage, theme === environment.theme { + closeImage = image + } else { + closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)! + state.cachedCloseImage = (closeImage, theme) + } + let closeButton = closeButton.update( + component: Button( + content: AnyComponent(Image(image: closeImage)), + action: { + component.dismiss() + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + context.add(closeButton + .position(CGPoint(x: context.availableSize.width - closeButton.size.width, y: 28.0)) + ) + + let titleString: String + let amountTitle: String + let amountPlaceholder: String + + let minAmount: Int64? + let maxAmount: Int64? + + let configuration = StarsWithdrawConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) + + switch component.mode { + case let .withdraw(status): + titleString = environment.strings.Stars_Withdraw_Title + amountTitle = environment.strings.Stars_Withdraw_AmountTitle + amountPlaceholder = environment.strings.Stars_Withdraw_AmountPlaceholder + + minAmount = configuration.minWithdrawAmount + maxAmount = status.balances.availableBalance + case .paidMedia: + titleString = environment.strings.Stars_PaidContent_Title + amountTitle = environment.strings.Stars_PaidContent_AmountTitle + amountPlaceholder = environment.strings.Stars_PaidContent_AmountPlaceholder + + minAmount = 1 + maxAmount = configuration.maxPaidMediaAmount + case .reaction: + titleString = environment.strings.Stars_SendStars_Title + amountTitle = environment.strings.Stars_SendStars_AmountTitle + amountPlaceholder = environment.strings.Stars_SendStars_AmountPlaceholder + + minAmount = 1 + //TODO: + maxAmount = configuration.maxPaidMediaAmount + } + + let title = title.update( + component: Text(text: titleString, font: Font.bold(17.0), color: theme.list.itemPrimaryTextColor), + 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)) + ) + contentSize.height += title.size.height + contentSize.height += 40.0 + + let balance: Int64? + if case .reaction = component.mode { + balance = state.balance + } else if case let .withdraw(starsState) = component.mode { + balance = starsState.balances.availableBalance + } else { + balance = nil + } + + if let balance { + let balanceTitle = balanceTitle.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Stars_Transfer_Balance, + font: Font.regular(14.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ), + availableSize: context.availableSize, + transition: .immediate + ) + let balanceValue = balanceValue.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: presentationStringsFormattedNumber(Int32(balance), environment.dateTimeFormat.groupingSeparator), + font: Font.semibold(16.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ), + availableSize: context.availableSize, + transition: .immediate + ) + let balanceIcon = balanceIcon.update( + component: BundleIconComponent(name: "Premium/Stars/StarSmall", tintColor: nil), + availableSize: context.availableSize, + transition: .immediate + ) + + let topBalanceOriginY = 11.0 + context.add(balanceTitle + .position(CGPoint(x: 16.0 + environment.safeInsets.left + balanceTitle.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height / 2.0)) + ) + context.add(balanceIcon + .position(CGPoint(x: 16.0 + environment.safeInsets.left + balanceIcon.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height + balanceValue.size.height / 2.0 + 1.0 + UIScreenPixel)) + ) + context.add(balanceValue + .position(CGPoint(x: 16.0 + environment.safeInsets.left + balanceIcon.size.width + 3.0 + balanceValue.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height + balanceValue.size.height / 2.0 + 2.0 - UIScreenPixel)) + ) + } + + let amountFont = Font.regular(13.0) + let amountTextColor = theme.list.freeTextColor + let amountMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), bold: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), link: MarkdownAttributeSet(font: amountFont, textColor: theme.list.itemAccentColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) + } + let amountFooter: AnyComponent? + switch component.mode { + case .paidMedia: + let amountInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_PaidContent_AmountInfo, attributes: amountMarkdownAttributes, textAlignment: .natural)) + if let range = amountInfoString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + amountInfoString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: amountInfoString.string)) + } + amountFooter = AnyComponent(MultilineTextComponent( + text: .plain(amountInfoString), + maximumNumberOfLines: 0, + highlightColor: environment.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: { attributes, _ in + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_PaidContent_AmountInfo_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + } + )) + case let .reaction(starsToTop): + let amountInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SendStars_AmountInfo("\(starsToTop ?? 0)").string, attributes: amountMarkdownAttributes, textAlignment: .natural)) + amountFooter = AnyComponent(MultilineTextComponent( + text: .plain(amountInfoString), + maximumNumberOfLines: 0 + )) + default: + amountFooter = nil + } + + let urlSection = urlSection.update( + component: ListSectionComponent( + theme: theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: amountTitle.uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: amountFooter, + items: [ + AnyComponentWithIdentity( + id: "amount", + component: AnyComponent( + AmountFieldComponent( + textColor: theme.list.itemPrimaryTextColor, + placeholderColor: theme.list.itemPlaceholderTextColor, + value: state.amount, + minValue: minAmount, + maxValue: maxAmount, + placeholderText: amountPlaceholder, + amountUpdated: { [weak state] amount in + state?.amount = amount + state?.updated() + }, + tag: amountTag + ) + ) + ) + ] + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(urlSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + urlSection.size.height / 2.0)) + .clipsToBounds(true) + .cornerRadius(10.0) + ) + contentSize.height += urlSection.size.height + contentSize.height += 32.0 + + let buttonString: String + if case .paidMedia = component.mode { + buttonString = environment.strings.Stars_PaidContent_Create + } else if let amount = state.amount { + buttonString = "\(environment.strings.Stars_Withdraw_Withdraw) # \(amount)" + } else { + buttonString = environment.strings.Stars_Withdraw_Withdraw + } + + if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme { + state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, theme) + } + + let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) + if let range = buttonAttributedString.string.range(of: "#"), let starImage = state.cachedStarImage?.0 { + buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string)) + } + + let controller = environment.controller + let button = button.update( + component: ButtonComponent( + background: ButtonComponent.Background( + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) + ), + isEnabled: (state.amount ?? 0) > 0, + displaysProgress: false, + action: { [weak state] in + if let controller = controller() as? StarsWithdrawScreen, let amount = state?.amount { + if let minAmount, amount < minAmount { + controller.presentMinAmountTooltip(minAmount) + } else { + controller.completion(amount) + controller.dismissAnimated() + } + } + } + ), + availableSize: CGSize(width: 361.0, height: 50), + transition: .immediate + ) + context.add(button + .clipsToBounds(true) + .cornerRadius(10.0) + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) + ) + contentSize.height += button.size.height + contentSize.height += 15.0 + + contentSize.height += max(environment.inputHeight, environment.safeInsets.bottom) + + return contentSize + } + } + + final class State: ComponentState { + private let context: AccountContext + private let mode: StarsWithdrawScreen.Mode + + fileprivate var amount: Int64? + + fileprivate var balance: Int64? + private var stateDisposable: Disposable? + + var cachedCloseImage: (UIImage, PresentationTheme)? + var cachedStarImage: (UIImage, PresentationTheme)? + var cachedChevronImage: (UIImage, PresentationTheme)? + + init( + context: AccountContext, + mode: StarsWithdrawScreen.Mode + ) { + self.context = context + self.mode = mode + + var amount: Int64? + switch mode { + case let .withdraw(stats): + amount = stats.balances.availableBalance + case let .paidMedia(initialValue): + amount = initialValue + case .reaction: + amount = nil + } + + self.amount = amount + + super.init() + + if case .reaction = self.mode, let starsContext = context.starsContext { + self.stateDisposable = (starsContext.state + |> deliverOnMainQueue).startStrict(next: { [weak self] state in + if let self, let balance = state?.balance { + self.balance = balance + self.updated() + } + }) + } + } + + deinit { + self.stateDisposable?.dispose() + } + } + + func makeState() -> State { + return State(context: self.context, mode: self.mode) + } +} + +private final class StarsWithdrawSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + private let context: AccountContext + private let mode: StarsWithdrawScreen.Mode + + init( + context: AccountContext, + mode: StarsWithdrawScreen.Mode + ) { + self.context = context + self.mode = mode + } + + static func ==(lhs: StarsWithdrawSheetComponent, rhs: StarsWithdrawSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.mode != rhs.mode { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + mode: context.component.mode, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + backgroundColor: .blur(.light), + followContentSizeChanges: false, + clipsContent: true, + isScrollEnabled: false, + 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)) + ) + + return context.availableSize + } + } +} + +public final class StarsWithdrawScreen: ViewControllerComponentContainer { + public enum Mode: Equatable { + case withdraw(StarsRevenueStats) + case paidMedia(Int64?) + case reaction(Int64?) + } + + private let context: AccountContext + fileprivate let completion: (Int64) -> Void + + public init( + context: AccountContext, + mode: StarsWithdrawScreen.Mode, + completion: @escaping (Int64) -> Void + ) { + self.context = context + self.completion = completion + + super.init( + context: context, + component: StarsWithdrawSheetComponent( + context: context, + mode: mode + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let view = self.node.hostView.findTaggedView(tag: amountTag) as? AmountFieldComponent.View { + Queue.mainQueue().after(0.01) { + view.activateInput() + view.selectAll() + } + } + } + + func presentMinAmountTooltip(_ minAmount: Int64) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .image( + image: UIImage(bundleImageName: "Premium/Stars/StarLarge")!, + title: nil, + text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(minAmount))).string, + round: false, + undoText: nil + ), + elevatedLayout: false, + position: .top, + action: { _ in return true}) + self.present(resultController, in: .window(.root)) + + if let view = self.node.hostView.findTaggedView(tag: amountTag) as? AmountFieldComponent.View { + view.animateError() + } + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} + +private let invalidAmountCharacters = CharacterSet.decimalDigits.inverted + +private final class AmountFieldComponent: Component { + typealias EnvironmentType = Empty + + let textColor: UIColor + let placeholderColor: UIColor + let value: Int64? + let minValue: Int64? + let maxValue: Int64? + let placeholderText: String + let amountUpdated: (Int64?) -> Void + let tag: AnyObject? + + init( + textColor: UIColor, + placeholderColor: UIColor, + value: Int64?, + minValue: Int64?, + maxValue: Int64?, + placeholderText: String, + amountUpdated: @escaping (Int64?) -> Void, + tag: AnyObject? = nil + ) { + self.textColor = textColor + self.placeholderColor = placeholderColor + self.value = value + self.minValue = minValue + self.maxValue = maxValue + self.placeholderText = placeholderText + self.amountUpdated = amountUpdated + self.tag = tag + } + + static func ==(lhs: AmountFieldComponent, rhs: AmountFieldComponent) -> Bool { + if lhs.textColor != rhs.textColor { + return false + } + if lhs.placeholderColor != rhs.placeholderColor { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.minValue != rhs.minValue { + return false + } + if lhs.maxValue != rhs.maxValue { + return false + } + if lhs.placeholderText != rhs.placeholderText { + return false + } + return true + } + + final class View: UIView, UITextFieldDelegate, ComponentTaggedView { + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + private let placeholderView: ComponentView + private let iconView: UIImageView + private let textField: TextFieldNodeView + + private var component: AmountFieldComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.placeholderView = ComponentView() + self.textField = TextFieldNodeView(frame: .zero) + + self.iconView = UIImageView(image: UIImage(bundleImageName: "Premium/Stars/StarLarge")) + + super.init(frame: frame) + + self.textField.delegate = self + self.textField.addTarget(self, action: #selector(self.textChanged(_:)), for: .editingChanged) + + self.addSubview(self.textField) + self.addSubview(self.iconView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func textChanged(_ sender: Any) { + let text = self.textField.text ?? "" + let amount: Int64? + if !text.isEmpty, let value = Int64(text) { + amount = value + } else { + amount = nil + } + self.component?.amountUpdated(amount) + self.placeholderView.view?.isHidden = !text.isEmpty + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if string.rangeOfCharacter(from: invalidAmountCharacters) != nil { + return false + } + var newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) + if newText == "0" || (newText.count > 1 && newText.hasPrefix("0")) { + newText.removeFirst() + textField.text = newText + self.textChanged(self.textField) + return false + } + + if let component = self.component { + let amount: Int64? + if !newText.isEmpty, let value = Int64(normalizeArabicNumeralString(newText, type: .western)) { + amount = value + } else { + amount = nil + } + if let amount, let maxAmount = component.maxValue, amount > maxAmount { + textField.text = "\(maxAmount)" + self.textChanged(self.textField) + self.animateError() + return false + } + } + return true + } + + func activateInput() { + self.textField.becomeFirstResponder() + } + + func selectAll() { + self.textField.selectAll(nil) + } + + func animateError() { + self.textField.layer.addShakeAnimation() + let hapticFeedback = HapticFeedback() + hapticFeedback.error() + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: { + let _ = hapticFeedback + }) + } + + func update(component: AmountFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.textField.textColor = component.textColor + if let value = component.value { + self.textField.text = "\(value)" + } else { + self.textField.text = "" + } + self.textField.font = Font.regular(17.0) + + self.textField.keyboardType = .numberPad + self.textField.returnKeyType = .done + self.textField.autocorrectionType = .no + self.textField.autocapitalizationType = .none + + self.component = component + self.state = state + + let size = CGSize(width: availableSize.width, height: 44.0) + + var leftInset: CGFloat = 15.0 + if let icon = self.iconView.image { + leftInset += icon.size.width + 6.0 + self.iconView.frame = CGRect(origin: CGPoint(x: 15.0, y: floorToScreenPixels((size.height - icon.size.height) / 2.0)), size: icon.size) + } + + let placeholderSize = self.placeholderView.update( + transition: .easeInOut(duration: 0.2), + component: AnyComponent( + Text( + text: component.placeholderText, + font: Font.regular(17.0), + color: component.placeholderColor + ) + ), + environment: {}, + containerSize: availableSize + ) + + if let placeholderComponentView = self.placeholderView.view { + if placeholderComponentView.superview == nil { + self.insertSubview(placeholderComponentView, at: 0) + } + + placeholderComponentView.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((size.height - placeholderSize.height) / 2.0) + 1.0 - UIScreenPixel), size: placeholderSize) + + placeholderComponentView.isHidden = !(self.textField.text ?? "").isEmpty + } + + self.textField.frame = CGRect(x: leftInset, y: 0.0, width: size.width - 30.0, height: 44.0) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + + +func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(foregroundColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} + +private struct StarsWithdrawConfiguration { + static var defaultValue: StarsWithdrawConfiguration { + return StarsWithdrawConfiguration(minWithdrawAmount: nil, maxPaidMediaAmount: nil) + } + + let minWithdrawAmount: Int64? + let maxPaidMediaAmount: Int64? + + fileprivate init(minWithdrawAmount: Int64?, maxPaidMediaAmount: Int64?) { + self.minWithdrawAmount = minWithdrawAmount + self.maxPaidMediaAmount = maxPaidMediaAmount + } + + static func with(appConfiguration: AppConfiguration) -> StarsWithdrawConfiguration { + if let data = appConfiguration.data { + var minWithdrawAmount: Int64? + if let value = data["stars_revenue_withdrawal_min"] as? Double { + minWithdrawAmount = Int64(value) + } + var maxPaidMediaAmount: Int64? + if let value = data["stars_paid_post_amount_max"] as? Double { + maxPaidMediaAmount = Int64(value) + } + + return StarsWithdrawConfiguration(minWithdrawAmount: minWithdrawAmount, maxPaidMediaAmount: maxPaidMediaAmount) + } else { + return .defaultValue + } + } +} diff --git a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift index 851c0342708..80ee4b55596 100644 --- a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift +++ b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift @@ -237,7 +237,7 @@ private final class StickerSelectionComponent: Component { self.state?.updated(transition: .easeInOut(duration: 0.2)) } - func update(component: StickerSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StickerSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.backgroundColor = component.backgroundColor let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85) self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate) @@ -421,7 +421,7 @@ private final class StickerSelectionComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -532,7 +532,7 @@ public class StickerPickerScreen: ViewController { self.containerView.addSubview(self.hostView) if controller.hasInteractiveStickers { - self.storyStickersContentView = StoryStickersContentView(frame: .zero) + self.storyStickersContentView = StoryStickersContentView(isPremium: context.isPremium) self.storyStickersContentView?.locationAction = { [weak self] in self?.controller?.presentLocationPicker() } @@ -542,8 +542,15 @@ public class StickerPickerScreen: ViewController { self.storyStickersContentView?.reactionAction = { [weak self] in self?.controller?.addReaction() } - self.storyStickersContentView?.cameraAction = { [weak self] in - self?.controller?.addCamera() + self.storyStickersContentView?.linkAction = { [weak self] in + guard let self, let controller = self.controller else { + return + } + if controller.context.isPremium { + controller.addLink() + } else { + self.presentLinkPremiumSuggestion() + } } } @@ -735,7 +742,7 @@ public class StickerPickerScreen: ViewController { let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - let gallery = GalleryController(context: context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in + let gallery = GalleryController(context: context, source: .standaloneMessage(message, nil), streamSingleVideo: true, replaceRootController: { _, _ in }, baseNavigationController: nil) gallery.setHintWillBePresentedInPreviewingContext(true) @@ -799,7 +806,7 @@ public class StickerPickerScreen: ViewController { controller.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { [weak controller] action in if case .info = action, let controller { let premiumController = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs, forceDark: controller.forceDark, dismissed: nil) - controller.push(premiumController) + controller.pushController(premiumController) return true } return false @@ -1633,8 +1640,34 @@ public class StickerPickerScreen: ViewController { self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) } } + + func presentLinkPremiumSuggestion() { + guard let controller = self.controller else { + return + } + let tooltipController = UndoOverlayController( + presentationData: self.presentationData, + content: .linkCopied( + text: self.presentationData.strings.Story_Editor_TooltipLinkPremium + ), + elevatedLayout: true, + position: .top, + animateInAsReplacement: false, action: { [weak controller] action in + if case .info = action, let controller { + let _ = controller.completion(nil) + controller.dismiss(animated: true) + + let premiumController = controller.context.sharedContext.makePremiumIntroController(context: controller.context, source: .storiesLinks, forceDark: controller.forceDark, dismissed: nil) + controller.pushController(premiumController) + return true + } + return false + } + ) + controller.present(tooltipController, in: .window(.root)) + } - func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) { + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ComponentTransition) { guard let controller = self.controller else { return } @@ -1727,11 +1760,11 @@ public class StickerPickerScreen: ViewController { transition.setFrame(view: self.containerView, frame: clipFrame) if let content = self.content { - var stickersTransition: Transition = transition + var stickersTransition: ComponentTransition = transition if let scheduledEmojiContentAnimationHint = self.scheduledEmojiContentAnimationHint { self.scheduledEmojiContentAnimationHint = nil let contentAnimation = scheduledEmojiContentAnimationHint - stickersTransition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) + stickersTransition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) } var contentSize = self.hostView.update( @@ -1953,18 +1986,18 @@ public class StickerPickerScreen: ViewController { let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) } else { self.isExpanded = true - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) } } else if (velocity.y < -300.0 || offset < topInset / 2.0) { let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset) let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) self.isExpanded = true - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) } else { if let listNode = listNode { listNode.scroller.setContentOffset(CGPoint(), animated: false) @@ -1972,7 +2005,7 @@ public class StickerPickerScreen: ViewController { scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) } - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) } if !dismissing { @@ -1985,7 +2018,7 @@ public class StickerPickerScreen: ViewController { case .cancelled: self.panGestureArguments = nil - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) default: break } @@ -2000,7 +2033,7 @@ public class StickerPickerScreen: ViewController { guard let (layout, navigationHeight) = self.currentLayout else { return } - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) } } @@ -2029,7 +2062,7 @@ public class StickerPickerScreen: ViewController { public var presentLocationPicker: () -> Void = { } public var presentAudioPicker: () -> Void = { } public var addReaction: () -> Void = { } - public var addCamera: () -> Void = { } + public var addLink: () -> Void = { } public init(context: AccountContext, inputData: Signal, forceDark: Bool = false, expanded: Bool = false, defaultToEmoji: Bool = false, hasEmoji: Bool = true, hasGifs: Bool = false, hasInteractiveStickers: Bool = true) { self.context = context @@ -2099,7 +2132,7 @@ public class StickerPickerScreen: ViewController { let navigationHeight: CGFloat = 56.0 - self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) } } @@ -2168,7 +2201,7 @@ private final class InteractiveStickerButtonContent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: InteractiveStickerButtonContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: InteractiveStickerButtonContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.backgroundLayer.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.11).cgColor let iconSize = self.icon.update( @@ -2229,7 +2262,7 @@ private final class InteractiveStickerButtonContent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -2271,7 +2304,7 @@ private final class InteractiveReactionButtonContent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: InteractiveReactionButtonContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: InteractiveReactionButtonContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let bounds = CGRect(origin: .zero, size: CGSize(width: 54.0, height: 54.0)) let iconSize = self.icon.update( transition: .immediate, @@ -2299,7 +2332,7 @@ private final class InteractiveReactionButtonContent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -2344,7 +2377,7 @@ private final class RoundVideoButtonContent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: RoundVideoButtonContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: RoundVideoButtonContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.backgroundLayer.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.11).cgColor let bounds = CGRect(origin: .zero, size: CGSize(width: 54.0, height: 54.0)) @@ -2378,7 +2411,7 @@ private final class RoundVideoButtonContent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -2440,7 +2473,7 @@ final class ItemStack: CombinedComponent { let remainingWidth = context.availableSize.width - itemsWidth - context.component.padding * 2.0 let spacing = remainingWidth / CGFloat(rowItemsCount - 1) - if spacing < context.component.minSpacing { + if spacing < context.component.minSpacing || currentGroup.count == 2 { groups.append(currentGroup) currentGroup = [] } @@ -2493,24 +2526,58 @@ final class ItemStack: CombinedComponent { } } - final class StoryStickersContentView: UIView, EmojiCustomContentView { let tintContainerView = UIView() private let container = ComponentView() + private let isPremium: Bool + var locationAction: () -> Void = {} var audioAction: () -> Void = {} var reactionAction: () -> Void = {} - var cameraAction: () -> Void = {} + var linkAction: () -> Void = {} + + init(isPremium: Bool) { + self.isPremium = isPremium + + super.init(frame: .zero) + } - func update(theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, availableSize: CGSize, transition: Transition) -> CGSize { + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let padding: CGFloat = 22.0 let size = self.container.update( transition: transition, component: AnyComponent( ItemStack( [ + AnyComponentWithIdentity( + id: "link", + component: AnyComponent( + CameraButton( + content: AnyComponentWithIdentity( + id: "content", + component: AnyComponent( + InteractiveStickerButtonContent( + theme: theme, + title: strings.MediaEditor_AddLink, + iconName: self.isPremium ? "Media Editor/Link" : "Media Editor/LinkLocked", + useOpaqueTheme: useOpaqueTheme, + tintContainerView: self.tintContainerView + ) + ) + ), + action: { [weak self] in + if let self { + self.linkAction() + } + }) + ) + ), AnyComponentWithIdentity( id: "location", component: AnyComponent( diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataButtonComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataButtonComponent.swift index f88407b6f45..b6ad5a27566 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataButtonComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataButtonComponent.swift @@ -95,7 +95,7 @@ final class DataButtonComponent: Component { component.action() } - func update(component: DataButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: DataButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme self.component = component @@ -133,7 +133,7 @@ final class DataButtonComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataCategoriesComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataCategoriesComponent.swift index 5faf5376389..cacfe2e0bfa 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataCategoriesComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataCategoriesComponent.swift @@ -91,7 +91,7 @@ final class DataCategoriesComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: DataCategoriesComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: DataCategoriesComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -186,7 +186,7 @@ final class DataCategoriesComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataCategoryItemCompoment.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataCategoryItemCompoment.swift index 0e7f6e926df..fe49ed2ee15 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataCategoryItemCompoment.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataCategoryItemCompoment.swift @@ -119,7 +119,7 @@ private final class SubItemComponent: Component { return result } - func update(component: SubItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: SubItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme || self.component?.isIncoming != component.isIncoming self.component = component @@ -230,7 +230,7 @@ private final class SubItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -368,7 +368,7 @@ final class DataCategoryItemComponent: Component { return result } - func update(component: DataCategoryItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: DataCategoryItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme || self.component?.category.color != component.category.color self.component = component @@ -605,7 +605,7 @@ final class DataCategoryItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift index d26fa0d6428..fcc7febb34b 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift @@ -433,7 +433,7 @@ final class DataUsageScreenComponent: Component { func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { let scrollBounds = self.scrollView.bounds if let headerView = self.segmentedControlView.view, let navigationMetrics = self.navigationMetrics { @@ -445,7 +445,7 @@ final class DataUsageScreenComponent: Component { headerOffset = min(headerOffset, minOffset) - let animatedTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + let animatedTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) let navigationBackgroundAlpha: CGFloat = abs(headerOffset - minOffset) < 4.0 ? 1.0 : 0.0 let navigationButtonAlpha: CGFloat = scrollBounds.minY >= navigationMetrics.navigationHeight ? 0.0 : 1.0 @@ -485,7 +485,7 @@ final class DataUsageScreenComponent: Component { } } - func update(component: DataUsageScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: DataUsageScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -690,7 +690,7 @@ final class DataUsageScreenComponent: Component { if transition.animation.isImmediate, let animationHint { switch animationHint.value { case .modeChanged, .clearedItems: - pieChartTransition = Transition(animation: .curve(duration: 0.4, curve: .spring)) + pieChartTransition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)) } } @@ -876,7 +876,7 @@ final class DataUsageScreenComponent: Component { var labelTransition = transition if labelTransition.animation.isImmediate, let animationHint, animationHint.value == .modeChanged { - labelTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) + labelTransition = ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)) } let chartTotalLabelSize = self.chartTotalLabel.update( @@ -922,7 +922,7 @@ final class DataUsageScreenComponent: Component { return } self.selectedStats = id - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(value: .modeChanged))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(value: .modeChanged))) })), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -953,7 +953,7 @@ final class DataUsageScreenComponent: Component { } else { self.expandedCategories.insert(key) } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } )), environment: {}, @@ -1234,8 +1234,8 @@ final class DataUsageScreenComponent: Component { #endif self.allStats = StatsSet() - //self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: .clearedItems))) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(value: .clearedItems))) + //self.state?.updated(transition: ComponentTransition(animation: .none).withUserData(AnimationHint(value: .clearedItems))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(value: .clearedItems))) } } @@ -1243,7 +1243,7 @@ final class DataUsageScreenComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift index 4878d848c53..8392185a9a2 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift @@ -114,7 +114,7 @@ private final class ChartSelectionTooltip: Component { preconditionFailure() } - func update(component: ChartSelectionTooltip, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChartSelectionTooltip, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let sideInset: CGFloat = 10.0 let height: CGFloat = 24.0 @@ -155,7 +155,7 @@ private final class ChartSelectionTooltip: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -1292,11 +1292,11 @@ final class PieChartComponent: Component { } else { self.selectedKey = nil } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } } - func update(component: PieChartComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PieChartComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let dataUpdated = self.component?.chartData != component.chartData self.state = state @@ -1389,7 +1389,7 @@ final class PieChartComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/SegmentControlComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/SegmentControlComponent.swift index e6ff6db6793..8a65b0887fe 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/SegmentControlComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/SegmentControlComponent.swift @@ -66,7 +66,7 @@ final class SegmentControlComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: SegmentControlComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: SegmentControlComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme self.component = component @@ -106,7 +106,7 @@ final class SegmentControlComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoriesComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoriesComponent.swift index 44056145f02..2643e14d8d9 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoriesComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoriesComponent.swift @@ -101,7 +101,7 @@ final class StorageCategoriesComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: StorageCategoriesComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StorageCategoriesComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -270,7 +270,7 @@ final class StorageCategoriesComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoryItemCompoment.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoryItemCompoment.swift index 10e3360b82d..d22a9711d8b 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoryItemCompoment.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoryItemCompoment.swift @@ -165,7 +165,7 @@ final class StorageCategoryItemComponent: Component { return result } - func update(component: StorageCategoryItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StorageCategoryItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme || self.component?.category.color != component.category.color self.component = component @@ -409,7 +409,7 @@ final class StorageCategoryItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift index 0c5fd3d39a7..771c64a6419 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift @@ -270,11 +270,11 @@ private final class FileListItemComponent: Component { } self.isExtractedToContextMenu = value - let mappedTransition: Transition + let mappedTransition: ComponentTransition if value { - mappedTransition = Transition(transition) + mappedTransition = ComponentTransition(transition) } else { - mappedTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + mappedTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) } self.state?.updated(transition: mappedTransition) } @@ -329,7 +329,7 @@ private final class FileListItemComponent: Component { component.action(component.messageId) } - func update(component: FileListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: FileListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme var hasSelectionUpdated = false @@ -622,7 +622,7 @@ private final class FileListItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -799,7 +799,7 @@ final class StorageFileListPanelComponent: Component { cancelContextGestures(view: scrollView) } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else { return } @@ -1003,7 +1003,7 @@ final class StorageFileListPanelComponent: Component { } } - func update(component: StorageFileListPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StorageFileListPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let environment = environment[StorageUsagePanelEnvironment.self].value @@ -1069,7 +1069,7 @@ final class StorageFileListPanelComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageKeepSizeComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageKeepSizeComponent.swift index 849ae4d083e..4a22a48f0a9 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageKeepSizeComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageKeepSizeComponent.swift @@ -101,7 +101,7 @@ final class StorageKeepSizeComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: StorageKeepSizeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StorageKeepSizeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme self.component = component @@ -206,7 +206,7 @@ final class StorageKeepSizeComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageMediaGridPanelComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageMediaGridPanelComponent.swift index 7d61c393cd7..17680079494 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageMediaGridPanelComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageMediaGridPanelComponent.swift @@ -150,7 +150,7 @@ private final class MediaGridLayer: SimpleLayer { })?.cgImage } - func updateSelection(size: CGSize, selectionState: SelectionState, theme: PresentationTheme, transition: Transition) { + func updateSelection(size: CGSize, selectionState: SelectionState, theme: PresentationTheme, transition: ComponentTransition) { if self.size == size && self.selectionState == selectionState && self.theme === theme { return } @@ -535,7 +535,7 @@ final class StorageMediaGridPanelComponent: Component { } } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else { return } @@ -613,7 +613,7 @@ final class StorageMediaGridPanelComponent: Component { } } - func update(component: StorageMediaGridPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StorageMediaGridPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let environment = environment[StorageUsagePanelEnvironment.self].value @@ -657,7 +657,7 @@ final class StorageMediaGridPanelComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift index 6e9f125649c..0313c71a8d1 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift @@ -152,11 +152,11 @@ private final class PeerListItemComponent: Component { } self.isExtractedToContextMenu = value - let mappedTransition: Transition + let mappedTransition: ComponentTransition if value { - mappedTransition = Transition(transition) + mappedTransition = ComponentTransition(transition) } else { - mappedTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + mappedTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) } self.state?.updated(transition: mappedTransition) } @@ -211,7 +211,7 @@ private final class PeerListItemComponent: Component { component.action(peer) } - func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme var hasSelectionUpdated = false @@ -372,7 +372,7 @@ private final class PeerListItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -549,7 +549,7 @@ final class StoragePeerListPanelComponent: Component { cancelContextGestures(view: scrollView) } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else { return } @@ -635,7 +635,7 @@ final class StoragePeerListPanelComponent: Component { } } - func update(component: StoragePeerListPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoragePeerListPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let environment = environment[StorageUsagePanelEnvironment.self].value @@ -699,7 +699,7 @@ final class StoragePeerListPanelComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerTypeItemComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerTypeItemComponent.swift index 8e541801829..07c7c186762 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerTypeItemComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerTypeItemComponent.swift @@ -140,7 +140,7 @@ final class StoragePeerTypeItemComponent: Component { } func setHasAssociatedMenu(_ hasAssociatedMenu: Bool) { - let transition: Transition + let transition: ComponentTransition if hasAssociatedMenu { transition = .immediate } else { @@ -152,7 +152,7 @@ final class StoragePeerTypeItemComponent: Component { transition.setAlpha(view: self.arrowIconView, alpha: hasAssociatedMenu ? 0.5 : 1.0) } - func update(component: StoragePeerTypeItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoragePeerTypeItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme let previousComponent = self.component @@ -283,7 +283,7 @@ final class StoragePeerTypeItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift index f3edec47c69..6c50be3b614 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift @@ -217,7 +217,7 @@ private final class StorageUsageHeaderComponent: Component { } } - func update(component: StorageUsageHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StorageUsageHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme self.component = component @@ -316,7 +316,7 @@ private final class StorageUsageHeaderComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -345,7 +345,7 @@ final class StorageUsagePanelContainerComponent: Component { let dateTimeFormat: PresentationDateTimeFormat let insets: UIEdgeInsets let items: [Item] - let currentPanelUpdated: (AnyHashable, Transition) -> Void + let currentPanelUpdated: (AnyHashable, ComponentTransition) -> Void init( theme: PresentationTheme, @@ -353,7 +353,7 @@ final class StorageUsagePanelContainerComponent: Component { dateTimeFormat: PresentationDateTimeFormat, insets: UIEdgeInsets, items: [Item], - currentPanelUpdated: @escaping (AnyHashable, Transition) -> Void + currentPanelUpdated: @escaping (AnyHashable, ComponentTransition) -> Void ) { self.theme = theme self.strings = strings @@ -529,7 +529,7 @@ final class StorageUsagePanelContainerComponent: Component { } self.transitionFraction = 0.0 - let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring)) if let currentId = self.currentId { self.state?.updated(transition: transition) component.currentPanelUpdated(currentId, transition) @@ -544,12 +544,12 @@ final class StorageUsagePanelContainerComponent: Component { } } - func updateNavigationMergeFactor(value: CGFloat, transition: Transition) { + func updateNavigationMergeFactor(value: CGFloat, transition: ComponentTransition) { transition.setAlpha(view: self.topPanelMergedBackgroundView, alpha: value) transition.setAlpha(view: self.topPanelBackgroundView, alpha: 1.0 - value) } - func update(component: StorageUsagePanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StorageUsagePanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[StorageUsagePanelContainerEnvironment.self].value let themeUpdated = self.component?.theme !== component.theme @@ -615,7 +615,7 @@ final class StorageUsagePanelContainerComponent: Component { } if component.items.contains(where: { $0.id == id }) { self.currentId = id - let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring)) self.state?.updated(transition: transition) component.currentPanelUpdated(id, transition) } @@ -763,7 +763,7 @@ final class StorageUsagePanelContainerComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 85596c09108..1e4cebaf79c 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -907,7 +907,7 @@ final class StorageUsageScreenComponent: Component { } } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { let scrollBounds = self.scrollView.bounds let isLockedAtPanels = scrollBounds.maxY == self.scrollView.contentSize.height @@ -921,7 +921,7 @@ final class StorageUsageScreenComponent: Component { headerOffset = min(headerOffset, minOffset) - let animatedTransition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut)) + let animatedTransition = ComponentTransition(animation: .curve(duration: 0.18, curve: .easeInOut)) let navigationBackgroundAlpha: CGFloat = abs(headerOffset - minOffset) < 4.0 ? 1.0 : 0.0 animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) @@ -971,7 +971,7 @@ final class StorageUsageScreenComponent: Component { ) } - func update(component: StorageUsageScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StorageUsageScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -1098,7 +1098,7 @@ final class StorageUsageScreenComponent: Component { if let animationHint { if case .firstStatsUpdate = animationHint.value { - let alphaTransition: Transition + let alphaTransition: ComponentTransition if environment.isVisible { alphaTransition = .easeInOut(duration: 0.25) } else { @@ -1147,7 +1147,7 @@ final class StorageUsageScreenComponent: Component { } if let aggregatedData = self.aggregatedData, !aggregatedData.isSelectingPeers { aggregatedData.isSelectingPeers = true - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } } ).minSize(CGSize(width: 16.0, height: environment.navigationHeight - environment.statusBarHeight))), @@ -1171,7 +1171,7 @@ final class StorageUsageScreenComponent: Component { } aggregatedData.isSelectingPeers = false aggregatedData.clearPeerSelection() - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } ).minSize(CGSize(width: 16.0, height: environment.navigationHeight - environment.statusBarHeight))), environment: {}, @@ -1397,7 +1397,7 @@ final class StorageUsageScreenComponent: Component { var pieChartTransition = transition if transition.animation.isImmediate, let animationHint, case .clearedItems = animationHint.value { - pieChartTransition = Transition(animation: .curve(duration: 0.4, curve: .spring)) + pieChartTransition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)) } let pieChartSize = self.pieChartView.update( @@ -1701,7 +1701,7 @@ final class StorageUsageScreenComponent: Component { aggregatedData.setIsCategorySelected(category: key, isSelected: true) } } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) }, toggleOtherExpanded: { [weak self] in guard let self else { @@ -1709,7 +1709,7 @@ final class StorageUsageScreenComponent: Component { } self.isOtherCategoryExpanded = !self.isOtherCategoryExpanded - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) }, clearAction: { [weak self] in guard let self else { @@ -1992,7 +1992,7 @@ final class StorageUsageScreenComponent: Component { if aggregatedData.isSelectingPeers { aggregatedData.togglePeerSelection(id: peer.id) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } else { self.openPeer(peer: peer) } @@ -2057,7 +2057,7 @@ final class StorageUsageScreenComponent: Component { return } aggregatedData.togglePeerSelection(id: peer.id) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) }) )) let items = ContextController.Items(content: .list(itemList)) @@ -2093,7 +2093,7 @@ final class StorageUsageScreenComponent: Component { return } aggregatedData.toggleMessageSelection(id: messageId) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) }, contextAction: { [weak self] messageId, containerView, sourceRect, gesture in guard let self else { @@ -2120,7 +2120,7 @@ final class StorageUsageScreenComponent: Component { return } aggregatedData.toggleMessageSelection(id: messageId) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) }, contextAction: { [weak self] messageId, containerView, gesture in guard let self else { @@ -2147,7 +2147,7 @@ final class StorageUsageScreenComponent: Component { return } aggregatedData.toggleMessageSelection(id: messageId) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) }, contextAction: { [weak self] messageId, containerView, gesture in guard let self else { @@ -2253,7 +2253,7 @@ final class StorageUsageScreenComponent: Component { } if delay == 0.0 { - let animationTransition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + let animationTransition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)) animationTransition.setAlpha(view: clearingNode.view, alpha: 0.0, completion: { [weak clearingNode] _ in clearingNode?.removeFromSupernode() }) @@ -2329,7 +2329,7 @@ final class StorageUsageScreenComponent: Component { if firstTime { self.aggregatedData = initialAggregatedData - self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: .firstStatsUpdate))) + self.state?.updated(transition: ComponentTransition(animation: .none).withUserData(AnimationHint(value: .firstStatsUpdate))) self.component?.ready.set(.single(true)) } @@ -2470,9 +2470,9 @@ final class StorageUsageScreenComponent: Component { self.isClearing = false if !firstTime { - self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: .clearedItems))) + self.state?.updated(transition: ComponentTransition(animation: .none).withUserData(AnimationHint(value: .clearedItems))) } else { - self.state?.updated(transition: Transition(animation: .none)) + self.state?.updated(transition: ComponentTransition(animation: .none)) } completion() @@ -2588,7 +2588,7 @@ final class StorageUsageScreenComponent: Component { return } aggregatedData.toggleMessageSelection(id: message.id) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) }))) switch previewData { @@ -2692,7 +2692,7 @@ final class StorageUsageScreenComponent: Component { return } aggregatedData.toggleMessageSelection(id: message.id) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) }) )) let items = ContextController.Items(content: .list(itemList)) @@ -3272,7 +3272,7 @@ final class StorageUsageScreenComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift index 5cd33daa875..72ab6cdebfa 100644 --- a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift @@ -5,6 +5,381 @@ import ComponentFlow import HierarchyTrackingLayer import TelegramPresentationData +private extension CGFloat { + func remap(fromLow: CGFloat, fromHigh: CGFloat, toLow: CGFloat, toHigh: CGFloat) -> CGFloat { + guard (fromHigh - fromLow) != 0 else { + // Would produce NAN + return 0 + } + return toLow + (self - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + } +} + +private extension CGPoint { + /// Returns the length between the receiver and *CGPoint.zero* + var vectorLength: CGFloat { + distanceTo(.zero) + } + + var isZero: Bool { + x == 0 && y == 0 + } + + /// Operator convenience to divide points with / + static func / (lhs: CGPoint, rhs: CGFloat) -> CGPoint { + CGPoint(x: lhs.x / CGFloat(rhs), y: lhs.y / CGFloat(rhs)) + } + + /// Operator convenience to multiply points with * + static func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint { + CGPoint(x: lhs.x * CGFloat(rhs), y: lhs.y * CGFloat(rhs)) + } + + /// Operator convenience to add points with + + static func +(left: CGPoint, right: CGPoint) -> CGPoint { + left.add(right) + } + + /// Operator convenience to subtract points with - + static func -(left: CGPoint, right: CGPoint) -> CGPoint { + left.subtract(right) + } + + /// Returns the distance between the receiver and the given point. + func distanceTo(_ a: CGPoint) -> CGFloat { + let xDist = a.x - x + let yDist = a.y - y + return CGFloat(sqrt((xDist * xDist) + (yDist * yDist))) + } + + func rounded(decimal: CGFloat) -> CGPoint { + CGPoint(x: round(decimal * x) / decimal, y: round(decimal * y) / decimal) + } + + func interpolate(to: CGPoint, amount: CGFloat) -> CGPoint { + return self + ((to - self) * amount) + } + + func interpolate( + _ to: CGPoint, + outTangent: CGPoint, + inTangent: CGPoint, + amount: CGFloat, + maxIterations: Int = 3, + samples: Int = 20, + accuracy: CGFloat = 1) + -> CGPoint + { + if amount == 0 { + return self + } + if amount == 1 { + return to + } + + if + colinear(outTangent, inTangent) == true, + outTangent.colinear(inTangent, to) == true + { + return interpolate(to: to, amount: amount) + } + + let step = 1 / CGFloat(samples) + + var points: [(point: CGPoint, distance: CGFloat)] = [(point: self, distance: 0)] + var totalLength: CGFloat = 0 + + var previousPoint = self + var previousAmount = CGFloat(0) + + var closestPoint = 0 + + while previousAmount < 1 { + + previousAmount = previousAmount + step + + if previousAmount < amount { + closestPoint = closestPoint + 1 + } + + let newPoint = pointOnPath(to, outTangent: outTangent, inTangent: inTangent, amount: previousAmount) + let distance = previousPoint.distanceTo(newPoint) + totalLength = totalLength + distance + points.append((point: newPoint, distance: totalLength)) + previousPoint = newPoint + } + + let accurateDistance = amount * totalLength + var point = points[closestPoint] + + var foundPoint = false + + var pointAmount = CGFloat(closestPoint) * step + var nextPointAmount: CGFloat = pointAmount + step + + var refineIterations = 0 + while foundPoint == false { + refineIterations = refineIterations + 1 + /// First see if the next point is still less than the projected length. + let nextPoint = points[min(closestPoint + 1, points.indices.last!)] + if nextPoint.distance < accurateDistance { + point = nextPoint + closestPoint = closestPoint + 1 + pointAmount = CGFloat(closestPoint) * step + nextPointAmount = pointAmount + step + if closestPoint == points.count { + foundPoint = true + } + continue + } + if accurateDistance < point.distance { + closestPoint = closestPoint - 1 + if closestPoint < 0 { + foundPoint = true + continue + } + point = points[closestPoint] + pointAmount = CGFloat(closestPoint) * step + nextPointAmount = pointAmount + step + continue + } + + /// Now we are certain the point is the closest point under the distance + let pointDiff = nextPoint.distance - point.distance + let proposedPointAmount = ((accurateDistance - point.distance) / pointDiff) + .remap(fromLow: 0, fromHigh: 1, toLow: pointAmount, toHigh: nextPointAmount) + + let newPoint = pointOnPath(to, outTangent: outTangent, inTangent: inTangent, amount: proposedPointAmount) + let newDistance = point.distance + point.point.distanceTo(newPoint) + pointAmount = proposedPointAmount + point = (point: newPoint, distance: newDistance) + if + accurateDistance - newDistance <= accuracy || + newDistance - accurateDistance <= accuracy + { + foundPoint = true + } + + if refineIterations == maxIterations { + foundPoint = true + } + } + return point.point + } + + func pointOnPath(_ to: CGPoint, outTangent: CGPoint, inTangent: CGPoint, amount: CGFloat) -> CGPoint { + let a = interpolate(to: outTangent, amount: amount) + let b = outTangent.interpolate(to: inTangent, amount: amount) + let c = inTangent.interpolate(to: to, amount: amount) + let d = a.interpolate(to: b, amount: amount) + let e = b.interpolate(to: c, amount: amount) + let f = d.interpolate(to: e, amount: amount) + return f + } + + func colinear(_ a: CGPoint, _ b: CGPoint) -> Bool { + let area = x * (a.y - b.y) + a.x * (b.y - y) + b.x * (y - a.y); + let accuracy: CGFloat = 0.05 + if area < accuracy && area > -accuracy { + return true + } + return false + } + + /// Subtracts the given point from the receiving point. + func subtract(_ point: CGPoint) -> CGPoint { + CGPoint( + x: x - point.x, + y: y - point.y) + } + + /// Adds the given point from the receiving point. + func add(_ point: CGPoint) -> CGPoint { + CGPoint( + x: x + point.x, + y: y + point.y) + } +} + +private extension CurveVertex { + func interpolate(to: CurveVertex, amount: CGFloat) -> CurveVertex { + CurveVertex( + point: point.interpolate(to: to.point, amount: amount), + inTangent: inTangent.interpolate(to: to.inTangent, amount: amount), + outTangent: outTangent.interpolate(to: to.outTangent, amount: amount)) + } +} + +private struct CurveVertex { + init(_ inTangent: CGPoint, _ point: CGPoint, _ outTangent: CGPoint) { + self.point = point + self.inTangent = inTangent + self.outTangent = outTangent + } + + init(point: CGPoint, inTangentRelative: CGPoint, outTangentRelative: CGPoint) { + self.point = point + inTangent = CGPoint(x: point.x + inTangentRelative.x, y: point.y + inTangentRelative.y) + outTangent = CGPoint(x: point.x + outTangentRelative.x, y: point.y + outTangentRelative.y) + } + + init(point: CGPoint, inTangent: CGPoint, outTangent: CGPoint) { + self.point = point + self.inTangent = inTangent + self.outTangent = outTangent + } + + // MARK: Internal + + let point: CGPoint + + var inTangent: CGPoint + var outTangent: CGPoint + + var inTangentRelative: CGPoint { + return CGPoint(x: inTangent.x - point.x, y: inTangent.y - point.y) + } + + var outTangentRelative: CGPoint { + return CGPoint(x: outTangent.x - point.x, y: outTangent.y - point.y) + } + + func reversed() -> CurveVertex { + return CurveVertex(point: point, inTangent: outTangent, outTangent: inTangent) + } + + func translated(_ translation: CGPoint) -> CurveVertex { + return CurveVertex(point: CGPoint(x: point.x + translation.x, y: point.y + translation.y), inTangent: CGPoint(x: inTangent.x + translation.x, y: inTangent.y + translation.y), outTangent: CGPoint(x: outTangent.x + translation.x, y: outTangent.y + translation.y)) + } + + /// Trims a path defined by two Vertices at a specific position, from 0 to 1 + /// + /// The path can be visualized below. + /// + /// F is fromVertex. + /// V is the vertex of the receiver. + /// P is the position from 0-1. + /// O is the outTangent of fromVertex. + /// F====O=========P=======I====V + /// + /// After trimming the curve can be visualized below. + /// + /// S is the returned Start vertex. + /// E is the returned End vertex. + /// T is the trim point. + /// TI and TO are the new tangents for the trimPoint + /// NO and NI are the new tangents for the startPoint and endPoints + /// S==NO=========TI==T==TO=======NI==E + func splitCurve(toVertex: CurveVertex, position: CGFloat) -> + (start: CurveVertex, trimPoint: CurveVertex, end: CurveVertex) + { + + /// If position is less than or equal to 0, trim at start. + if position <= 0 { + return ( + start: CurveVertex(point: point, inTangentRelative: inTangentRelative, outTangentRelative: .zero), + trimPoint: CurveVertex(point: point, inTangentRelative: .zero, outTangentRelative: outTangentRelative), + end: toVertex) + } + + /// If position is greater than or equal to 1, trim at end. + if position >= 1 { + return ( + start: self, + trimPoint: CurveVertex( + point: toVertex.point, + inTangentRelative: toVertex.inTangentRelative, + outTangentRelative: .zero), + end: CurveVertex( + point: toVertex.point, + inTangentRelative: .zero, + outTangentRelative: toVertex.outTangentRelative)) + } + + if outTangentRelative == CGPoint() && toVertex.inTangentRelative == CGPoint() { + /// If both tangents are zero, then span to be trimmed is a straight line. + let trimPoint = point.interpolate(to: toVertex.point, amount: position) + return ( + start: self, + trimPoint: CurveVertex(point: trimPoint, inTangentRelative: .zero, outTangentRelative: .zero), + end: toVertex) + } + /// Cutting by amount gives incorrect length.... + /// One option is to cut by a stride until it gets close then edge it down. + /// Measuring a percentage of the spans does not equal the same as measuring a percentage of length. + /// This is where the historical trim path bugs come from. + let a = point.interpolate(to: outTangent, amount: position) + let b = outTangent.interpolate(to: toVertex.inTangent, amount: position) + let c = toVertex.inTangent.interpolate(to: toVertex.point, amount: position) + let d = a.interpolate(to: b, amount: position) + let e = b.interpolate(to: c, amount: position) + let f = d.interpolate(to: e, amount: position) + return ( + start: CurveVertex(point: point, inTangent: inTangent, outTangent: a), + trimPoint: CurveVertex(point: f, inTangent: d, outTangent: e), + end: CurveVertex(point: toVertex.point, inTangent: c, outTangent: toVertex.outTangent)) + } + + /// Trims a curve of a known length to a specific length and returns the points. + /// + /// There is not a performant yet accurate way to cut a curve to a specific length. + /// This calls splitCurve(toVertex: position:) to split the curve and then measures + /// the length of the new curve. The function then iterates through the samples, + /// adjusting the position of the cut for a more precise cut. + /// Usually a single iteration is enough to get within 0.5 points of the desired + /// length. + /// + /// This function should probably live in PathElement, since it deals with curve + /// lengths. + func trimCurve(toVertex: CurveVertex, atLength: CGFloat, curveLength: CGFloat, maxSamples: Int, accuracy: CGFloat = 1) -> + (start: CurveVertex, trimPoint: CurveVertex, end: CurveVertex) + { + var currentPosition = atLength / curveLength + var results = splitCurve(toVertex: toVertex, position: currentPosition) + + if maxSamples == 0 { + return results + } + + for _ in 1...maxSamples { + let length = results.start.distanceTo(results.trimPoint) + let lengthDiff = atLength - length + /// Check if length is correct. + if lengthDiff < accuracy { + return results + } + let diffPosition = max(min((currentPosition / length) * lengthDiff, currentPosition * 0.5), currentPosition * -0.5) + currentPosition = diffPosition + currentPosition + results = splitCurve(toVertex: toVertex, position: currentPosition) + } + return results + } + + /// The distance from the receiver to the provided vertex. + /// + /// For lines (zeroed tangents) the distance between the two points is measured. + /// For curves the curve is iterated over by sample count and the points are measured. + /// This is ~99% accurate at a sample count of 30 + func distanceTo(_ toVertex: CurveVertex, sampleCount: Int = 25) -> CGFloat { + + if outTangentRelative.isZero && toVertex.inTangentRelative.isZero { + /// Return a linear distance. + return point.distanceTo(toVertex.point) + } + + var distance: CGFloat = 0 + + var previousPoint = point + for i in 0.., transition: Transition) -> CGSize { + func update(component: AvatarStoryIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -295,60 +670,160 @@ public final class AvatarStoryIndicatorComponent: Component { let radius = (diameter - component.activeLineWidth) * 0.5 self.indicatorView.image = generateImage(CGSize(width: imageDiameter, height: imageDiameter), rotatedContext: { size, context in + UIGraphicsPushContext(context) + defer { + UIGraphicsPopContext() + } + context.clear(CGRect(origin: CGPoint(), size: size)) context.setLineCap(.round) var locations: [CGFloat] = [0.0, 1.0] - if let counters = component.counters, counters.totalCount > 1, !component.isRoundedRect { - let center = CGPoint(x: size.width * 0.5, y: size.height * 0.5) - let spacing: CGFloat = component.activeLineWidth * 2.0 - let angularSpacing: CGFloat = spacing / radius - let circleLength = CGFloat.pi * 2.0 * radius - let segmentLength = (circleLength - spacing * CGFloat(counters.totalCount)) / CGFloat(counters.totalCount) - let segmentAngle = segmentLength / radius - - for pass in 0 ..< 2 { - context.resetClip() + if let counters = component.counters, counters.totalCount > 1 { + if component.isRoundedRect { + let lineWidth: CGFloat = component.hasUnseen ? component.activeLineWidth : component.inactiveLineWidth + context.setLineWidth(lineWidth) + let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: size.width * 0.5 - diameter * 0.5, y: size.height * 0.5 - diameter * 0.5), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), cornerRadius: floor(diameter * 0.27)) - if pass == 0 { - context.setLineWidth(component.inactiveLineWidth) - } else { - context.setLineWidth(component.activeLineWidth) - } + var startPoint: CGPoint? + var vertices: [CurveVertex] = [] + path.cgPath.applyWithBlock({ element in + switch element.pointee.type { + case .moveToPoint: + startPoint = element.pointee.points[0] + case .addLineToPoint: + if let _ = vertices.last { + vertices.append(CurveVertex(point: element.pointee.points[0], inTangentRelative: CGPoint(), outTangentRelative: CGPoint())) + } else if let startPoint { + vertices.append(CurveVertex(point: startPoint, inTangentRelative: CGPoint(), outTangentRelative: CGPoint())) + vertices.append(CurveVertex(point: element.pointee.points[0], inTangentRelative: CGPoint(), outTangentRelative: CGPoint())) + } + case .addQuadCurveToPoint: + break + case .addCurveToPoint: + if let _ = vertices.last { + vertices.append(CurveVertex(point: element.pointee.points[2], inTangentRelative: CGPoint(), outTangentRelative: CGPoint())) + } else if let startPoint { + vertices.append(CurveVertex(point: startPoint, inTangentRelative: CGPoint(), outTangentRelative: CGPoint())) + vertices.append(CurveVertex(point: element.pointee.points[2], inTangentRelative: CGPoint(), outTangentRelative: CGPoint())) + } + + if vertices.count >= 2 { + vertices[vertices.count - 2].outTangent = element.pointee.points[0] + vertices[vertices.count - 1].inTangent = element.pointee.points[1] + } + case .closeSubpath: + if let startPointValue = startPoint { + vertices.append(CurveVertex(point: startPointValue, inTangentRelative: CGPoint(), outTangentRelative: CGPoint())) + startPoint = nil + } + @unknown default: + break + } + }) - let startIndex: Int - let endIndex: Int - if pass == 0 { - startIndex = 0 - endIndex = counters.totalCount - counters.unseenCount - } else { - startIndex = counters.totalCount - counters.unseenCount - endIndex = counters.totalCount - } - if startIndex < endIndex { - for i in startIndex ..< endIndex { - let startAngle = CGFloat(i) * (angularSpacing + segmentAngle) - CGFloat.pi * 0.5 + angularSpacing * 0.5 - context.move(to: CGPoint(x: center.x + cos(startAngle) * radius, y: center.y + sin(startAngle) * radius)) - context.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: startAngle + segmentAngle, clockwise: false) + var length: CGFloat = 0.0 + var firstOffset: CGFloat = 0.0 + for i in 0 ..< vertices.count - 1 { + let value = vertices[i].distanceTo(vertices[i + 1]) + if firstOffset == 0.0 { + firstOffset = value * 0.5 } - - context.replacePathWithStrokedPath() - context.clip() + length += value + } + + let spacing: CGFloat = component.activeLineWidth * 2.0 + let useableLength = length - spacing * CGFloat(counters.totalCount) + let segmentLength = useableLength / CGFloat(counters.totalCount) + + context.setLineWidth(lineWidth) + + for index in 0 ..< counters.totalCount { + var dashWidths: [CGFloat] = [] + dashWidths.append(segmentLength) + dashWidths.append(10000000.0) let colors: [CGColor] - if pass == 1 { + if index >= counters.totalCount - counters.unseenCount { colors = activeColors } else { colors = inactiveColors } - let colorSpace = CGColorSpaceCreateDeviceRGB() - if let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations) { + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.resetClip() + context.setLineDash(phase: -firstOffset - spacing * 0.5 - CGFloat(index) * (spacing + segmentLength), lengths: dashWidths) + context.addPath(path.cgPath) + + context.replacePathWithStrokedPath() + context.clip() + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + + if index == counters.totalCount - 1 { + context.resetClip() + let addPath = CGMutablePath() + addPath.move(to: CGPoint(x: vertices[0].interpolate(to: vertices[1], amount: 0.5).point.x - spacing * 0.5, y: vertices[0].point.y)) + addPath.addLine(to: CGPoint(x: vertices[0].point.x, y: vertices[0].point.y)) + context.setLineDash(phase: 0.0, lengths: []) + context.addPath(addPath) + context.replacePathWithStrokedPath() + context.clip() context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) } } + } else { + let center = CGPoint(x: size.width * 0.5, y: size.height * 0.5) + let spacing: CGFloat = component.activeLineWidth * 2.0 + let angularSpacing: CGFloat = spacing / radius + let circleLength = CGFloat.pi * 2.0 * radius + let segmentLength = (circleLength - spacing * CGFloat(counters.totalCount)) / CGFloat(counters.totalCount) + let segmentAngle = segmentLength / radius + + for pass in 0 ..< 2 { + context.resetClip() + + if pass == 0 { + context.setLineWidth(component.inactiveLineWidth) + } else { + context.setLineWidth(component.activeLineWidth) + } + + let startIndex: Int + let endIndex: Int + if pass == 0 { + startIndex = 0 + endIndex = counters.totalCount - counters.unseenCount + } else { + startIndex = counters.totalCount - counters.unseenCount + endIndex = counters.totalCount + } + if startIndex < endIndex { + for i in startIndex ..< endIndex { + let startAngle = CGFloat(i) * (angularSpacing + segmentAngle) - CGFloat.pi * 0.5 + angularSpacing * 0.5 + context.move(to: CGPoint(x: center.x + cos(startAngle) * radius, y: center.y + sin(startAngle) * radius)) + context.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: startAngle + segmentAngle, clockwise: false) + } + + context.replacePathWithStrokedPath() + context.clip() + + let colors: [CGColor] + if pass == 1 { + colors = activeColors + } else { + colors = inactiveColors + } + + let colorSpace = CGColorSpaceCreateDeviceRGB() + if let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations) { + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + } + } + } } } else { let lineWidth: CGFloat = component.hasUnseen ? component.activeLineWidth : component.inactiveLineWidth @@ -379,7 +854,7 @@ public final class AvatarStoryIndicatorComponent: Component { let indicatorFrame = CGRect(origin: CGPoint(x: (availableSize.width - imageDiameter) * 0.5, y: (availableSize.height - imageDiameter) * 0.5), size: CGSize(width: imageDiameter, height: imageDiameter)) transition.setFrame(view: self.indicatorView, frame: indicatorFrame) - let progressTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) + let progressTransition = ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)) if let progress = component.progress, !component.isRoundedRect { let colorLayer: SimpleGradientLayer if let current = self.colorLayer { @@ -448,7 +923,7 @@ public final class AvatarStoryIndicatorComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift b/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift index 170788ca402..6191ae72eea 100644 --- a/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift @@ -91,7 +91,7 @@ public final class ForwardInfoPanelComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ForwardInfoPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ForwardInfoPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -202,7 +202,7 @@ public final class ForwardInfoPanelComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index e32a2575a95..7c2d2b3aa8b 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -410,11 +410,11 @@ public final class PeerListItemComponent: Component { } self.isExtractedToContextMenu = value - let mappedTransition: Transition + let mappedTransition: ComponentTransition if value { - mappedTransition = Transition(transition) + mappedTransition = ComponentTransition(transition) } else { - mappedTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + mappedTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) } self.state?.updated(transition: mappedTransition) } @@ -522,7 +522,7 @@ public final class PeerListItemComponent: Component { self.imageNode?.isHidden = isPreviewing } - func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component var synchronousLoad = false @@ -1134,7 +1134,7 @@ public final class PeerListItemComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index c75d5360a25..4251599d658 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -97,6 +97,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent", "//submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen", "//submodules/TelegramUI/Components/SliderContextItem", + "//submodules/TelegramUI/Components/InteractiveTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaAreaMaskView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaAreaMaskView.swift new file mode 100644 index 00000000000..d2db1f1f212 --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaAreaMaskView.swift @@ -0,0 +1,45 @@ +import Foundation +import UIKit +import Display +import TelegramCore + +final class MediaAreaMaskLayer: CALayer { + private var params: (referenceSize: CGSize, mediaAreas: [MediaArea])? + + func update(referenceSize: CGSize, mediaAreas: [MediaArea], borderMaskLayer: CALayer?) { + guard referenceSize != self.params?.referenceSize && mediaAreas != self.params?.mediaAreas else { + return + } + + for mediaArea in mediaAreas { + let size = CGSize(width: mediaArea.coordinates.width / 100.0 * referenceSize.width, height: mediaArea.coordinates.height / 100.0 * referenceSize.height) + let position = CGPoint(x: mediaArea.coordinates.x / 100.0 * referenceSize.width, y: mediaArea.coordinates.y / 100.0 * referenceSize.height) + let cornerRadius: CGFloat + if let radius = mediaArea.coordinates.cornerRadius { + cornerRadius = radius / 100.0 * size.width + } else { + cornerRadius = size.height * 0.18 + } + + let layer = CALayer() + layer.backgroundColor = UIColor.white.cgColor + layer.bounds = CGRect(origin: .zero, size: size) + layer.position = position + layer.cornerRadius = cornerRadius + layer.transform = CATransform3DMakeRotation(mediaArea.coordinates.rotation * Double.pi / 180.0, 0.0, 0.0, 1.0) + self.addSublayer(layer) + + if let borderMaskLayer { + let borderLayer = CAShapeLayer() + borderLayer.strokeColor = UIColor.white.cgColor + borderLayer.fillColor = UIColor.clear.cgColor + borderLayer.lineWidth = 2.0 + borderLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: size), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil) + borderLayer.bounds = CGRect(origin: .zero, size: size) + borderLayer.position = position + borderLayer.transform = layer.transform + borderMaskLayer.addSublayer(borderLayer) + } + } + } +} diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift index 1fad133d46c..0fdf7b421f6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift @@ -152,7 +152,7 @@ final class MediaNavigationStripComponent: Component { fatalError("init(coder:) has not been implemented") } - func updateCurrentItemProgress(value: CGFloat, isBuffering: Bool, transition: Transition) { + func updateCurrentItemProgress(value: CGFloat, isBuffering: Bool, transition: ComponentTransition) { guard let component = self.component else { return } @@ -170,7 +170,7 @@ final class MediaNavigationStripComponent: Component { } private var isTransitioning = false - func update(component: MediaNavigationStripComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: MediaNavigationStripComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component @@ -291,7 +291,7 @@ final class MediaNavigationStripComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift index c8e338546a7..2c660af7495 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift @@ -14,7 +14,7 @@ public extension StoryContainerScreen { |> take(1) |> mapToSignal { state -> Signal in if let slice = state.slice { - return waitUntilStoryMediaPreloaded(context: context, peerId: slice.peer.id, storyItem: slice.item.storyItem) + return waitUntilStoryMediaPreloaded(context: context, peerId: slice.effectivePeer.id, storyItem: slice.item.storyItem) |> timeout(4.0, queue: .mainQueue(), alternate: .complete()) |> map { _ -> Void in } @@ -185,7 +185,7 @@ public extension StoryContainerScreen { } #endif - return waitUntilStoryMediaPreloaded(context: context, peerId: slice.peer.id, storyItem: slice.item.storyItem) + return waitUntilStoryMediaPreloaded(context: context, peerId: slice.effectivePeer.id, storyItem: slice.item.storyItem) |> timeout(4.0, queue: .mainQueue(), alternate: .complete()) |> map { _ -> StoryContentContextState in } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryActionsComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryActionsComponent.swift index af11956e7cf..958c24efe02 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryActionsComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryActionsComponent.swift @@ -63,7 +63,7 @@ public final class StoryActionsComponent: Component { let scale: CGFloat = highlighted ? 0.6 : 1.0 - let transition = Transition(animation: .curve(duration: highlighted ? 0.5 : 0.3, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: highlighted ? 0.5 : 0.3, curve: .spring)) transition.setSublayerTransform(view: self, transform: CATransform3DMakeScale(scale, scale, 1.0)) transition.setScale(view: self.maskBackgroundView, scale: scale) } @@ -81,7 +81,7 @@ public final class StoryActionsComponent: Component { self.action(item) } - func update(item: Item, size: CGSize, transition: Transition) { + func update(item: Item, size: CGSize, transition: ComponentTransition) { if self.item == item { return } @@ -125,7 +125,7 @@ public final class StoryActionsComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: StoryActionsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryActionsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.componentState = state @@ -175,7 +175,7 @@ public final class StoryActionsComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift index 3f3fe6dbcbe..488a9953f83 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift @@ -83,7 +83,7 @@ final class StoryAuthorInfoComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: StoryAuthorInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryAuthorInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -281,7 +281,7 @@ final class StoryAuthorInfoComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAvatarInfoComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAvatarInfoComponent.swift index 86bf129c96d..f3c2fa910eb 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAvatarInfoComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAvatarInfoComponent.swift @@ -44,7 +44,7 @@ final class StoryAvatarInfoComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: StoryAvatarInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryAvatarInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -66,7 +66,7 @@ final class StoryAvatarInfoComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index dc8f056aabd..e3e4aaaeacb 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -401,14 +401,14 @@ public final class StoryContentContextImpl: StoryContentContext { if let focusedIndex { self.storedFocusedId = mappedItems[focusedIndex].id - var previousItemId: Int32? - var nextItemId: Int32? + var previousItemId: StoryId? + var nextItemId: StoryId? if focusedIndex != 0 { - previousItemId = mappedItems[focusedIndex - 1].id + previousItemId = StoryId(peerId: peerId, id: mappedItems[focusedIndex - 1].id) } if focusedIndex != mappedItems.count - 1 { - nextItemId = mappedItems[focusedIndex + 1].id + nextItemId = StoryId(peerId: peerId, id: mappedItems[focusedIndex + 1].id) } let mappedFocusedIndex = peerStoryItemsView.items.firstIndex(where: { $0.id == mappedItems[focusedIndex].id }) @@ -440,11 +440,13 @@ public final class StoryContentContextImpl: StoryContentContext { let allItems = mappedItems.map { item in return StoryContentItem( + id: StoryId(peerId: peer.id, id: item.id), position: nil, dayCounters: nil, peerId: peer.id, storyItem: item, - entityFiles: extractItemEntityFiles(item: item, allEntityFiles: allEntityFiles) + entityFiles: extractItemEntityFiles(item: item, allEntityFiles: allEntityFiles), + itemPeer: nil ) } @@ -453,11 +455,13 @@ public final class StoryContentContextImpl: StoryContentContext { peer: peer, additionalPeerData: additionalPeerData, item: StoryContentItem( + id: StoryId(peerId: peer.id, id: mappedItem.id), position: mappedFocusedIndex ?? focusedIndex, dayCounters: nil, peerId: peer.id, storyItem: mappedItem, - entityFiles: extractItemEntityFiles(item: mappedItem, allEntityFiles: allEntityFiles) + entityFiles: extractItemEntityFiles(item: mappedItem, allEntityFiles: allEntityFiles), + itemPeer: nil ), totalCount: totalCount, previousItemId: previousItemId, @@ -1083,15 +1087,15 @@ public final class StoryContentContextImpl: StoryContentContext { switch direction { case .previous: if let previousItemId = slice.previousItemId { - currentState.centralPeerContext.currentFocusedId = previousItemId + currentState.centralPeerContext.currentFocusedId = previousItemId.id } case .next: if let nextItemId = slice.nextItemId { - currentState.centralPeerContext.currentFocusedId = nextItemId + currentState.centralPeerContext.currentFocusedId = nextItemId.id } case let .id(id): - if slice.allItems.contains(where: { $0.storyItem.id == id }) { - currentState.centralPeerContext.currentFocusedId = id + if slice.allItems.contains(where: { $0.id == id }) { + currentState.centralPeerContext.currentFocusedId = id.id } } } @@ -1340,11 +1344,13 @@ public final class SingleStoryContentContextImpl: StoryContentContext { ) let mainItem = StoryContentItem( + id: StoryId(peerId: peer.id, id: mappedItem.id), position: 0, dayCounters: nil, peerId: peer.id, storyItem: mappedItem, - entityFiles: extractItemEntityFiles(item: mappedItem, allEntityFiles: allEntityFiles) + entityFiles: extractItemEntityFiles(item: mappedItem, allEntityFiles: allEntityFiles), + itemPeer: nil ) let stateValue = StoryContentContextState( slice: StoryContentContextState.FocusedSlice( @@ -1403,6 +1409,36 @@ public final class SingleStoryContentContextImpl: StoryContentContext { } public final class PeerStoryListContentContextImpl: StoryContentContext { + private struct DayIndex: Hashable { + var year: Int32 + var day: Int32 + + init(timestamp: Int32) { + var time: time_t = time_t(timestamp) + var timeinfo: tm = tm() + localtime_r(&time, &timeinfo) + + self.year = timeinfo.tm_year + self.day = timeinfo.tm_yday + } + } + + private struct PeerData { + let data: (TelegramEngine.EngineData.Item.Peer.Peer.Result, + TelegramEngine.EngineData.Item.Peer.Presence.Result, + TelegramEngine.EngineData.Item.Peer.AreVoiceMessagesAvailable.Result, + TelegramEngine.EngineData.Item.Peer.CanViewStats.Result, + TelegramEngine.EngineData.Item.Peer.NotificationSettings.Result, + TelegramEngine.EngineData.Item.NotificationSettings.Global.Result, + TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging.Result, + TelegramEngine.EngineData.Item.Peer.BoostsToUnrestrict.Result, + TelegramEngine.EngineData.Item.Peer.AppliedBoosts.Result) + + init(data: (TelegramEngine.EngineData.Item.Peer.Peer.Result, TelegramEngine.EngineData.Item.Peer.Presence.Result, TelegramEngine.EngineData.Item.Peer.AreVoiceMessagesAvailable.Result, TelegramEngine.EngineData.Item.Peer.CanViewStats.Result, TelegramEngine.EngineData.Item.Peer.NotificationSettings.Result, TelegramEngine.EngineData.Item.NotificationSettings.Global.Result, TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging.Result, TelegramEngine.EngineData.Item.Peer.BoostsToUnrestrict.Result, TelegramEngine.EngineData.Item.Peer.AppliedBoosts.Result)) { + self.data = data + } + } + private let context: AccountContext public private(set) var stateValue: StoryContentContextState? @@ -1417,23 +1453,24 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { } private var storyDisposable: Disposable? + private var storyDataDisposable = MetaDisposable() private var requestedStoryKeys = Set() private var requestStoryDisposables = DisposableSet() - private var listState: PeerStoryListContext.State? + private var listState: StoryListContext.State? - private var focusedId: Int32? + private var focusedId: StoryId? private var focusedIdUpdated = Promise(Void()) private var preloadStoryResourceDisposables: [EngineMedia.Id: Disposable] = [:] private var pollStoryMetadataDisposables = DisposableSet() - public init(context: AccountContext, peerId: EnginePeer.Id, listContext: PeerStoryListContext, initialId: Int32?) { + private var currentPeerData: (EnginePeer.Id, Promise)? + + public init(context: AccountContext, listContext: StoryListContext, initialId: Int32?, splitIndexIntoDays: Bool) { self.context = context - context.engine.account.viewTracker.refreshCanSendMessagesForPeerIds(peerIds: [peerId]) - let preferHighQualityStories: Signal = combineLatest( context.sharedContext.automaticMediaDownloadSettings |> map { settings in @@ -1451,62 +1488,28 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { |> distinctUntilChanged self.storyDisposable = (combineLatest(queue: .mainQueue(), - context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), - TelegramEngine.EngineData.Item.Peer.Presence(id: peerId), - TelegramEngine.EngineData.Item.Peer.AreVoiceMessagesAvailable(id: peerId), - TelegramEngine.EngineData.Item.Peer.CanViewStats(id: peerId), - TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId), - TelegramEngine.EngineData.Item.NotificationSettings.Global(), - TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging(id: peerId), - TelegramEngine.EngineData.Item.Peer.BoostsToUnrestrict(id: peerId), - TelegramEngine.EngineData.Item.Peer.AppliedBoosts(id: peerId) - ), listContext.state, self.focusedIdUpdated.get(), preferHighQualityStories ) - |> deliverOnMainQueue).startStrict(next: { [weak self] data, state, _, preferHighQualityStories in + |> deliverOnMainQueue).startStrict(next: { [weak self] state, _, preferHighQualityStories in guard let self else { return } - let (peer, presence, areVoiceMessagesAvailable, canViewStats, notificationSettings, globalNotificationSettings, isPremiumRequiredForMessaging, boostsToUnrestrict, appliedBoosts) = data - - guard let peer else { - return - } - - let isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer._asPeer(), peerSettings: notificationSettings._asNotificationSettings(), topSearchPeers: []) - - let additionalPeerData = StoryContentContextState.AdditionalPeerData( - isMuted: isMuted, - areVoiceMessagesAvailable: areVoiceMessagesAvailable, - presence: presence, - canViewStats: canViewStats, - isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, - preferHighQualityStories: preferHighQualityStories, - boostsToUnrestrict: boostsToUnrestrict, - appliedBoosts: appliedBoosts - ) - - self.listState = state - let focusedIndex: Int? if let current = self.focusedId { if let index = state.items.firstIndex(where: { $0.id == current }) { focusedIndex = index - } else if let index = state.items.firstIndex(where: { $0.id <= current }) { - focusedIndex = index } else if !state.items.isEmpty { focusedIndex = 0 } else { focusedIndex = nil } } else if let initialId = initialId { - if let index = state.items.firstIndex(where: { $0.id == initialId }) { + if let index = state.items.firstIndex(where: { $0.storyItem.id == initialId }) { focusedIndex = index - } else if let index = state.items.firstIndex(where: { $0.id <= initialId }) { + } else if let index = state.items.firstIndex(where: { $0.storyItem.id <= initialId }) { focusedIndex = index } else { focusedIndex = nil @@ -1519,185 +1522,230 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { } } - struct DayIndex: Hashable { - var year: Int32 - var day: Int32 - - init(timestamp: Int32) { - var time: time_t = time_t(timestamp) - var timeinfo: tm = tm() - localtime_r(&time, &timeinfo) - - self.year = timeinfo.tm_year - self.day = timeinfo.tm_yday + let peerData: Signal + if let focusedIndex { + let peerId = state.items[focusedIndex].id.peerId + if let currentPeerData = self.currentPeerData, currentPeerData.0 == peerId { + peerData = currentPeerData.1.get() |> map(Optional.init) + } else { + context.engine.account.viewTracker.refreshCanSendMessagesForPeerIds(peerIds: [peerId]) + + let currentPeerData: (EnginePeer.Id, Promise) = (peerId, Promise()) + currentPeerData.1.set(context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), + TelegramEngine.EngineData.Item.Peer.Presence(id: peerId), + TelegramEngine.EngineData.Item.Peer.AreVoiceMessagesAvailable(id: peerId), + TelegramEngine.EngineData.Item.Peer.CanViewStats(id: peerId), + TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId), + TelegramEngine.EngineData.Item.NotificationSettings.Global(), + TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging(id: peerId), + TelegramEngine.EngineData.Item.Peer.BoostsToUnrestrict(id: peerId), + TelegramEngine.EngineData.Item.Peer.AppliedBoosts(id: peerId) + ) |> map { PeerData(data: $0) }) + self.currentPeerData = currentPeerData + + peerData = currentPeerData.1.get() |> map(Optional.init) } + } else { + peerData = .single(nil) } - let stateValue: StoryContentContextState - if let focusedIndex = focusedIndex { - let item = state.items[focusedIndex] - self.focusedId = item.id - - var allItems: [StoryContentItem] = [] + self.storyDataDisposable.set((peerData + |> deliverOnMainQueue).start(next: { [weak self] data in + guard let self else { + return + } - var dayCounts: [DayIndex: Int] = [:] - var itemDayIndices: [Int32: (Int, DayIndex)] = [:] + self.listState = state - for i in 0 ..< state.items.count { - let stateItem = state.items[i] - allItems.append(StoryContentItem( - position: i, - dayCounters: nil, - peerId: peer.id, - storyItem: stateItem, - entityFiles: extractItemEntityFiles(item: stateItem, allEntityFiles: state.allEntityFiles) - )) + let stateValue: StoryContentContextState + if let focusedIndex, let (peer, presence, areVoiceMessagesAvailable, canViewStats, notificationSettings, globalNotificationSettings, isPremiumRequiredForMessaging, boostsToUnrestrict, appliedBoosts) = data?.data, let peer { + let isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer._asPeer(), peerSettings: notificationSettings._asNotificationSettings(), topSearchPeers: []) + let additionalPeerData = StoryContentContextState.AdditionalPeerData( + isMuted: isMuted, + areVoiceMessagesAvailable: areVoiceMessagesAvailable, + presence: presence, + canViewStats: canViewStats, + isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, + preferHighQualityStories: preferHighQualityStories, + boostsToUnrestrict: boostsToUnrestrict, + appliedBoosts: appliedBoosts + ) - let day = DayIndex(timestamp: stateItem.timestamp) - let dayCount: Int - if let current = dayCounts[day] { - dayCount = current + 1 - dayCounts[day] = dayCount - } else { - dayCount = 1 - dayCounts[day] = dayCount + let item = state.items[focusedIndex] + self.focusedId = item.id + + var allItems: [StoryContentItem] = [] + + var dayCounts: [DayIndex: Int] = [:] + var itemDayIndices: [StoryId: (Int, DayIndex)] = [:] + + for i in 0 ..< state.items.count { + let stateItem = state.items[i] + allItems.append(StoryContentItem( + id: stateItem.id, + position: i, + dayCounters: nil, + peerId: stateItem.id.peerId, + storyItem: stateItem.storyItem, + entityFiles: extractItemEntityFiles(item: stateItem.storyItem, allEntityFiles: state.allEntityFiles), + itemPeer: stateItem.peer + )) + + let day: DayIndex + if splitIndexIntoDays { + day = DayIndex(timestamp: stateItem.storyItem.timestamp) + } else { + day = DayIndex(timestamp: 0) + } + let dayCount: Int + if let current = dayCounts[day] { + dayCount = current + 1 + dayCounts[day] = dayCount + } else { + dayCount = 1 + dayCounts[day] = dayCount + } + itemDayIndices[stateItem.id] = (dayCount - 1, day) } - itemDayIndices[stateItem.id] = (dayCount - 1, day) - } - - var dayCounters: StoryContentItem.DayCounters? - if let (offset, day) = itemDayIndices[item.id], let dayCount = dayCounts[day] { - dayCounters = StoryContentItem.DayCounters( - position: offset, - totalCount: dayCount + + var dayCounters: StoryContentItem.DayCounters? + if let (offset, day) = itemDayIndices[item.id], let dayCount = dayCounts[day] { + dayCounters = StoryContentItem.DayCounters( + position: offset, + totalCount: dayCount + ) + } + + stateValue = StoryContentContextState( + slice: StoryContentContextState.FocusedSlice( + peer: peer, + additionalPeerData: additionalPeerData, + item: StoryContentItem( + id: item.id, + position: focusedIndex, + dayCounters: dayCounters, + peerId: item.id.peerId, + storyItem: item.storyItem, + entityFiles: extractItemEntityFiles(item: item.storyItem, allEntityFiles: state.allEntityFiles), + itemPeer: item.peer + ), + totalCount: state.totalCount, + previousItemId: focusedIndex == 0 ? nil : state.items[focusedIndex - 1].id, + nextItemId: (focusedIndex == state.items.count - 1) ? nil : state.items[focusedIndex + 1].id, + allItems: allItems, + forwardInfoStories: [:] + ), + previousSlice: nil, + nextSlice: nil + ) + } else { + self.focusedId = nil + + stateValue = StoryContentContextState( + slice: nil, + previousSlice: nil, + nextSlice: nil ) } - stateValue = StoryContentContextState( - slice: StoryContentContextState.FocusedSlice( - peer: peer, - additionalPeerData: additionalPeerData, - item: StoryContentItem( - position: focusedIndex, - dayCounters: dayCounters, - peerId: peer.id, - storyItem: item, - entityFiles: extractItemEntityFiles(item: item, allEntityFiles: state.allEntityFiles) - ), - totalCount: state.totalCount, - previousItemId: focusedIndex == 0 ? nil : state.items[focusedIndex - 1].id, - nextItemId: (focusedIndex == state.items.count - 1) ? nil : state.items[focusedIndex + 1].id, - allItems: allItems, - forwardInfoStories: [:] - ), - previousSlice: nil, - nextSlice: nil - ) - } else { - self.focusedId = nil - - stateValue = StoryContentContextState( - slice: nil, - previousSlice: nil, - nextSlice: nil - ) - } - - if self.stateValue == nil || self.stateValue?.slice != stateValue.slice { - self.stateValue = stateValue - self.statePromise.set(.single(stateValue)) - self.updatedPromise.set(.single(Void())) - - var resultResources: [EngineMedia.Id: StoryPreloadInfo] = [:] - var pollItems: [StoryKey] = [] - - if let focusedIndex, let slice = stateValue.slice { - var possibleItems: [(EnginePeer, EngineStoryItem)] = [] - if peer.id == self.context.account.peerId { - pollItems.append(StoryKey(peerId: peer.id, id: slice.item.storyItem.id)) - } + if self.stateValue == nil || self.stateValue?.slice != stateValue.slice { + self.stateValue = stateValue + self.statePromise.set(.single(stateValue)) + self.updatedPromise.set(.single(Void())) - for i in focusedIndex ..< min(focusedIndex + 4, state.items.count) { - if i != focusedIndex { - possibleItems.append((slice.peer, state.items[i])) + var resultResources: [EngineMedia.Id: StoryPreloadInfo] = [:] + var pollItems: [StoryKey] = [] + + if let focusedIndex, let slice = stateValue.slice { + var possibleItems: [(EnginePeer, StoryListContext.State.Item)] = [] + if slice.item.id.peerId == self.context.account.peerId { + pollItems.append(StoryKey(peerId: slice.item.id.peerId, id: slice.item.id.id)) } - if slice.peer.id == self.context.account.peerId { - pollItems.append(StoryKey(peerId: slice.peer.id, id: state.items[i].id)) + for i in focusedIndex ..< min(focusedIndex + 4, state.items.count) { + if i != focusedIndex { + possibleItems.append((slice.peer, state.items[i])) + } + + if slice.peer.id == self.context.account.peerId { + pollItems.append(StoryKey(peerId: slice.peer.id, id: state.items[i].storyItem.id)) + } } - } - - var nextPriority = 0 - for i in 0 ..< min(possibleItems.count, 3) { - let peer = possibleItems[i].0 - let item = possibleItems[i].1 - if let peerReference = PeerReference(peer._asPeer()), let mediaId = item.media.id { - var reactions: [MessageReaction.Reaction] = [] - for mediaArea in item.mediaAreas { - if case let .reaction(_, reaction, _) = mediaArea { - if !reactions.contains(reaction) { - reactions.append(reaction) + + var nextPriority = 0 + for i in 0 ..< min(possibleItems.count, 3) { + let peer = possibleItems[i].0 + let item = possibleItems[i].1 + if let peerReference = PeerReference(peer._asPeer()), let mediaId = item.storyItem.media.id { + var reactions: [MessageReaction.Reaction] = [] + for mediaArea in item.storyItem.mediaAreas { + if case let .reaction(_, reaction, _) = mediaArea { + if !reactions.contains(reaction) { + reactions.append(reaction) + } } } + + var selectedMedia: EngineMedia + if let alternativeMedia = item.storyItem.alternativeMedia, (!preferHighQualityStories && !item.storyItem.isMy) { + selectedMedia = alternativeMedia + } else { + selectedMedia = item.storyItem.media + } + + resultResources[mediaId] = StoryPreloadInfo( + peer: peerReference, + storyId: item.storyItem.id, + media: selectedMedia, + reactions: reactions, + priority: .top(position: nextPriority) + ) + nextPriority += 1 } - - var selectedMedia: EngineMedia - if let alternativeMedia = item.alternativeMedia, (!preferHighQualityStories && !item.isMy) { - selectedMedia = alternativeMedia - } else { - selectedMedia = item.media + } + } + + var validIds: [EngineMedia.Id] = [] + for (_, info) in resultResources.sorted(by: { $0.value.priority < $1.value.priority }) { + if let mediaId = info.media.id { + validIds.append(mediaId) + if self.preloadStoryResourceDisposables[mediaId] == nil { + self.preloadStoryResourceDisposables[mediaId] = preloadStoryMedia(context: context, info: info).startStrict() } - - resultResources[mediaId] = StoryPreloadInfo( - peer: peerReference, - storyId: item.id, - media: selectedMedia, - reactions: reactions, - priority: .top(position: nextPriority) - ) - nextPriority += 1 } } - } - - var validIds: [EngineMedia.Id] = [] - for (_, info) in resultResources.sorted(by: { $0.value.priority < $1.value.priority }) { - if let mediaId = info.media.id { - validIds.append(mediaId) - if self.preloadStoryResourceDisposables[mediaId] == nil { - self.preloadStoryResourceDisposables[mediaId] = preloadStoryMedia(context: context, info: info).startStrict() + + var removeIds: [EngineMedia.Id] = [] + for (id, disposable) in self.preloadStoryResourceDisposables { + if !validIds.contains(id) { + removeIds.append(id) + disposable.dispose() } } - } - - var removeIds: [EngineMedia.Id] = [] - for (id, disposable) in self.preloadStoryResourceDisposables { - if !validIds.contains(id) { - removeIds.append(id) - disposable.dispose() + for id in removeIds { + self.preloadStoryResourceDisposables.removeValue(forKey: id) } - } - for id in removeIds { - self.preloadStoryResourceDisposables.removeValue(forKey: id) - } - - var pollIdByPeerId: [EnginePeer.Id: [Int32]] = [:] - for storyKey in pollItems.prefix(3) { - if pollIdByPeerId[storyKey.peerId] == nil { - pollIdByPeerId[storyKey.peerId] = [storyKey.id] - } else { - pollIdByPeerId[storyKey.peerId]?.append(storyKey.id) + + var pollIdByPeerId: [EnginePeer.Id: [Int32]] = [:] + for storyKey in pollItems.prefix(3) { + if pollIdByPeerId[storyKey.peerId] == nil { + pollIdByPeerId[storyKey.peerId] = [storyKey.id] + } else { + pollIdByPeerId[storyKey.peerId]?.append(storyKey.id) + } + } + for (peerId, ids) in pollIdByPeerId { + self.pollStoryMetadataDisposables.add(self.context.engine.messages.refreshStoryViews(peerId: peerId, ids: ids).startStrict()) } } - for (peerId, ids) in pollIdByPeerId { - self.pollStoryMetadataDisposables.add(self.context.engine.messages.refreshStoryViews(peerId: peerId, ids: ids).startStrict()) - } - } + })) }) } deinit { self.storyDisposable?.dispose() + self.storyDataDisposable.dispose() self.requestStoryDisposables.dispose() for (_, disposable) in self.preloadStoryResourceDisposables { @@ -2487,14 +2535,14 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { if let focusedIndex { self.storedFocusedId = mappedItems[focusedIndex].id - var previousItemId: Int32? - var nextItemId: Int32? + var previousItemId: StoryId? + var nextItemId: StoryId? if focusedIndex != 0 { - previousItemId = mappedItems[focusedIndex - 1].id + previousItemId = StoryId(peerId: peerId, id: mappedItems[focusedIndex - 1].id) } if focusedIndex != mappedItems.count - 1 { - nextItemId = mappedItems[focusedIndex + 1].id + nextItemId = StoryId(peerId: peerId, id: mappedItems[focusedIndex + 1].id) } let mappedFocusedIndex = mappedItems.firstIndex(where: { $0.id == mappedItems[focusedIndex].id }) @@ -2512,11 +2560,13 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { let allItems = mappedItems.map { item in return StoryContentItem( + id: StoryId(peerId: peer.id, id: item.id), position: nil, dayCounters: nil, peerId: peer.id, storyItem: item, - entityFiles: extractItemEntityFiles(item: item, allEntityFiles: allEntityFiles) + entityFiles: extractItemEntityFiles(item: item, allEntityFiles: allEntityFiles), + itemPeer: nil ) } @@ -2525,11 +2575,13 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { peer: peer, additionalPeerData: additionalPeerData, item: StoryContentItem( + id: StoryId(peerId: peer.id, id: mappedItem.id), position: mappedFocusedIndex ?? focusedIndex, dayCounters: nil, peerId: peer.id, storyItem: mappedItem, - entityFiles: extractItemEntityFiles(item: mappedItem, allEntityFiles: allEntityFiles) + entityFiles: extractItemEntityFiles(item: mappedItem, allEntityFiles: allEntityFiles), + itemPeer: nil ), totalCount: totalCount, previousItemId: previousItemId, @@ -2973,15 +3025,15 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { switch direction { case .previous: if let previousItemId = slice.previousItemId { - currentState.centralPeerContext.currentFocusedId = previousItemId + currentState.centralPeerContext.currentFocusedId = previousItemId.id } case .next: if let nextItemId = slice.nextItemId { - currentState.centralPeerContext.currentFocusedId = nextItemId + currentState.centralPeerContext.currentFocusedId = nextItemId.id } case let .id(id): - if slice.allItems.contains(where: { $0.storyItem.id == id }) { - currentState.centralPeerContext.currentFocusedId = id + if slice.allItems.contains(where: { $0.id == id }) { + currentState.centralPeerContext.currentFocusedId = id.id } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 2be200e6434..6416406beb5 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -425,7 +425,7 @@ private final class StoryContainerScreenComponent: Component { var longPressRecognizer: StoryLongPressRecognizer? - private var pendingNavigationToItemId: (peerId: EnginePeer.Id, id: Int32)? + private var pendingNavigationToItemId: StoryId? private let interactionGuide = ComponentView() private var isDisplayingInteractionGuide: Bool = false @@ -521,7 +521,7 @@ private final class StoryContainerScreenComponent: Component { guard let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { return } - guard let visibleItemView = itemSetComponentView.visibleItems[slice.item.storyItem.id]?.view.view as? StoryItemContentComponent.View else { + guard let visibleItemView = itemSetComponentView.visibleItems[slice.item.id]?.view.view as? StoryItemContentComponent.View else { return } @@ -563,7 +563,7 @@ private final class StoryContainerScreenComponent: Component { guard let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { return } - guard let visibleItemView = itemSetComponentView.visibleItems[slice.item.storyItem.id]?.view.view as? StoryItemContentComponent.View else { + guard let visibleItemView = itemSetComponentView.visibleItems[slice.item.id]?.view.view as? StoryItemContentComponent.View else { return } visibleItemView.seekEnded() @@ -629,7 +629,7 @@ private final class StoryContainerScreenComponent: Component { } self.itemSetPinchState = nil if !self.isUpdating { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } } self.addGestureRecognizer(pinchRecognizer) @@ -788,7 +788,7 @@ private final class StoryContainerScreenComponent: Component { self.itemSetPanState = ItemSetPanState(fraction: 0.0, didBegin: true) if !updateImmediately { if !self.isUpdating { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))) } } } else { @@ -881,7 +881,7 @@ private final class StoryContainerScreenComponent: Component { itemSetPanState.fraction = 0.0 self.itemSetPanState = itemSetPanState - let transition = Transition(animation: .curve(duration: 0.4, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)) if !self.isUpdating { self.state?.updated(transition: transition) } @@ -926,14 +926,14 @@ private final class StoryContainerScreenComponent: Component { if self.itemSetPanState == nil { self.itemSetPanState = ItemSetPanState(fraction: 0.0, didBegin: false) if !self.isUpdating { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))) } } case .cancelled, .ended: if let itemSetPanState = self.itemSetPanState, !itemSetPanState.didBegin { self.itemSetPanState = nil if !self.isUpdating { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))) } } default: @@ -1079,7 +1079,7 @@ private final class StoryContainerScreenComponent: Component { if !self.dismissWithoutTransitionOut, let component = self.component, let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View, let transitionOut = component.transitionOut(slice.peer.id, slice.item.storyItem.id) { self.state?.updated(transition: .immediate) - let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)) transition.setAlpha(layer: self.backgroundLayer, alpha: 0.0) transition.setAlpha(view: self.backgroundEffectView, alpha: 0.0) @@ -1100,11 +1100,11 @@ private final class StoryContainerScreenComponent: Component { transitionOut.completed() } - let transition: Transition + let transition: ComponentTransition if self.dismissWithoutTransitionOut { - transition = Transition(animation: .curve(duration: 0.5, curve: .spring)) + transition = ComponentTransition(animation: .curve(duration: 0.5, curve: .spring)) } else { - transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) } self.isDismissedExlusively = true @@ -1213,7 +1213,7 @@ private final class StoryContainerScreenComponent: Component { self.commitHorizontalPan(velocity: CGPoint(x: 200.0, y: 0.0)) } } else { - var mappedId: Int32? + var mappedId: StoryId? switch direction { case .previous: mappedId = slice.previousItemId @@ -1223,7 +1223,7 @@ private final class StoryContainerScreenComponent: Component { mappedId = id } if let mappedId { - self.pendingNavigationToItemId = (slice.peer.id, mappedId) + self.pendingNavigationToItemId = mappedId component.content.navigate(navigation: .item(.id(mappedId))) } } @@ -1240,7 +1240,7 @@ private final class StoryContainerScreenComponent: Component { self.environment?.controller()?.present(tooltipScreen, in: .current) } - func update(component: StoryContainerScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryContainerScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.didAnimateOut { return availableSize } @@ -1805,8 +1805,8 @@ private final class StoryContainerScreenComponent: Component { return targetTransform } - Transition.immediate.setTransform(view: itemSetComponentView, transform: faceTransform) - Transition.immediate.setTransform(layer: itemSetView.tintLayer, transform: faceTransform) + ComponentTransition.immediate.setTransform(view: itemSetComponentView, transform: faceTransform) + ComponentTransition.immediate.setTransform(layer: itemSetView.tintLayer, transform: faceTransform) if let previousRotationFraction = itemSetView.rotationFraction, !itemSetTransition.animation.isImmediate { let fromT = previousRotationFraction @@ -1929,7 +1929,7 @@ private final class StoryContainerScreenComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -1953,12 +1953,12 @@ public class StoryContainerScreen: ViewControllerComponentContainer { public final class TransitionView { public let makeView: () -> UIView - public let updateView: (UIView, TransitionState, Transition) -> Void + public let updateView: (UIView, TransitionState, ComponentTransition) -> Void public let insertCloneTransitionView: ((UIView) -> Void)? public init( makeView: @escaping () -> UIView, - updateView: @escaping (UIView, TransitionState, Transition) -> Void, + updateView: @escaping (UIView, TransitionState, ComponentTransition) -> Void, insertCloneTransitionView: ((UIView) -> Void)? ) { self.makeView = makeView diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift index 3b4f6bbf43f..90479b4c0e9 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift @@ -99,27 +99,36 @@ public final class StoryContentItem: Equatable { } } + public let id: StoryId public let position: Int? public let dayCounters: DayCounters? public let peerId: EnginePeer.Id? public let storyItem: EngineStoryItem public let entityFiles: [EngineMedia.Id: TelegramMediaFile] + public let itemPeer: EnginePeer? public init( + id: StoryId, position: Int?, dayCounters: DayCounters?, peerId: EnginePeer.Id?, storyItem: EngineStoryItem, - entityFiles: [EngineMedia.Id: TelegramMediaFile] + entityFiles: [EngineMedia.Id: TelegramMediaFile], + itemPeer: EnginePeer? ) { + self.id = id self.position = position self.dayCounters = dayCounters self.peerId = peerId self.storyItem = storyItem self.entityFiles = entityFiles + self.itemPeer = itemPeer } public static func ==(lhs: StoryContentItem, rhs: StoryContentItem) -> Bool { + if lhs.id != rhs.id { + return false + } if lhs.position != rhs.position { return false } @@ -135,6 +144,9 @@ public final class StoryContentItem: Equatable { if lhs.entityFiles != rhs.entityFiles { return false } + if lhs.itemPeer != rhs.itemPeer { + return false + } return true } } @@ -204,18 +216,22 @@ public final class StoryContentContextState { public let additionalPeerData: AdditionalPeerData public let item: StoryContentItem public let totalCount: Int - public let previousItemId: Int32? - public let nextItemId: Int32? + public let previousItemId: StoryId? + public let nextItemId: StoryId? public let allItems: [StoryContentItem] public let forwardInfoStories: [StoryId: Promise] + var effectivePeer: EnginePeer { + return self.item.itemPeer ?? self.peer + } + public init( peer: EnginePeer, additionalPeerData: AdditionalPeerData, item: StoryContentItem, totalCount: Int, - previousItemId: Int32?, - nextItemId: Int32?, + previousItemId: StoryId?, + nextItemId: StoryId?, allItems: [StoryContentItem], forwardInfoStories: [StoryId: Promise] ) { @@ -274,7 +290,7 @@ public enum StoryContentContextNavigation { public enum ItemDirection { case previous case next - case id(Int32) + case id(StoryId) } public enum PeerDirection { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index 5c8132efaa2..2bfde6f2600 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -8,13 +8,13 @@ import Postbox import TelegramCore import TextNodeWithEntities import TextFormat -import InvisibleInkDustNode import UrlEscaping import TelegramPresentationData import TextSelectionNode import SwiftSignalKit import ForwardInfoPanelComponent import PlainButtonComponent +import InteractiveTextComponent final class StoryContentCaptionComponent: Component { enum Action { @@ -152,10 +152,8 @@ final class StoryContentCaptionComponent: Component { } private final class ContentItem: UIView { - var textNode: TextNodeWithEntities? - var spoilerTextNode: TextNodeWithEntities? + var textNode: InteractiveTextNodeWithEntities? var linkHighlightingNode: LinkHighlightingNode? - var dustNode: InvisibleInkDustNode? override init(frame: CGRect) { super.init(frame: frame) @@ -198,6 +196,8 @@ final class StoryContentCaptionComponent: Component { private var ignoreScrolling: Bool = false private var ignoreExternalState: Bool = false + private var displayContentsUnderSpoilers: (value: Bool, location: CGPoint?) = (false, nil) + private var expandedContentsBlocks: Set = Set() private var isExpanded: Bool = false private var codeHighlight: CachedMessageSyntaxHighlight? @@ -330,9 +330,9 @@ final class StoryContentCaptionComponent: Component { @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { if self.isExpanded { - self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.collapse(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } else { - self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.expand(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } } } @@ -352,7 +352,7 @@ final class StoryContentCaptionComponent: Component { } } - func expand(transition: Transition) { + func expand(transition: ComponentTransition) { self.ignoreScrolling = true if let textNode = self.expandedText.textNode?.textNode { var offset = textNode.frame.minY - 8.0 @@ -371,7 +371,7 @@ final class StoryContentCaptionComponent: Component { self.updateScrolling(transition: transition.withUserData(InternalTransitionHint(bounceScrolling: true))) } - func collapse(transition: Transition) { + func collapse(transition: ComponentTransition) { self.ignoreScrolling = true if transition.animation.isImmediate { @@ -391,7 +391,7 @@ final class StoryContentCaptionComponent: Component { self.textSelectionNode?.cancelSelection() } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let itemLayout = self.itemLayout else { return } @@ -449,20 +449,17 @@ final class StoryContentCaptionComponent: Component { } let contentItem = self.isExpanded ? self.expandedText : self.collapsedText - let otherContentItem = !self.isExpanded ? self.expandedText : self.collapsedText switch recognizer.state { case .ended: if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, let component = self.component, let textNode = contentItem.textNode { let titleFrame = textNode.textNode.view.bounds if titleFrame.contains(location) { - if let (index, attributes) = textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) { + let textLocalPoint = CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY) + if let (index, attributes) = textNode.textNode.attributesAtPoint(textLocalPoint) { let action: Action? - if case .tap = gesture, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(contentItem.dustNode?.isRevealed ?? true) { - let convertedPoint = recognizer.view?.convert(location, to: contentItem.dustNode?.view) ?? location - contentItem.dustNode?.revealAtLocation(convertedPoint) - otherContentItem.dustNode?.revealAtLocation(convertedPoint) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) + if case .tap = gesture, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !self.displayContentsUnderSpoilers.value { + self.updateDisplayContentsUnderSpoilers(value: true, at: recognizer.view?.convert(location, to: textNode.textNode.view) ?? location) return } else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { var concealed = true @@ -497,9 +494,19 @@ final class StoryContentCaptionComponent: Component { if component.externalState.isSelectingText { self.cancelTextSelection() } else if self.isExpanded { - self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + if let blockIndex = textNode.textNode.collapsibleBlockAtPoint(textLocalPoint) { + if self.expandedContentsBlocks.contains(blockIndex) { + self.expandedContentsBlocks.remove(blockIndex) + } else { + self.expandedContentsBlocks.insert(blockIndex) + } + self.state?.updated(transition: .spring(duration: 0.4)) + self.expand(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) + } else { + self.collapse(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) + } } else { - self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.expand(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } } } @@ -508,9 +515,9 @@ final class StoryContentCaptionComponent: Component { if component.externalState.isSelectingText { self.cancelTextSelection() } else if self.isExpanded { - self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.collapse(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } else { - self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.expand(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } } } @@ -519,9 +526,9 @@ final class StoryContentCaptionComponent: Component { if component.externalState.isSelectingText { self.cancelTextSelection() } else if self.isExpanded { - self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.collapse(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } else { - self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.expand(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } } } @@ -531,6 +538,14 @@ final class StoryContentCaptionComponent: Component { } } + private func updateDisplayContentsUnderSpoilers(value: Bool, at location: CGPoint?) { + if self.displayContentsUnderSpoilers.value == value { + return + } + self.displayContentsUnderSpoilers = (value, location) + self.state?.updated(transition: .easeInOut(duration: 0.2)) + } + private func updateTouchesAtPoint(_ point: CGPoint?) { let contentItem = self.isExpanded ? self.expandedText : self.collapsedText @@ -563,7 +578,7 @@ final class StoryContentCaptionComponent: Component { } } - if let spoilerRects = spoilerRects, !spoilerRects.isEmpty, let dustNode = contentItem.dustNode, !dustNode.isRevealed { + if let spoilerRects = spoilerRects, !spoilerRects.isEmpty, !self.displayContentsUnderSpoilers.value { } else if let rects = rects { let linkHighlightingNode: LinkHighlightingNode if let current = contentItem.linkHighlightingNode { @@ -583,7 +598,7 @@ final class StoryContentCaptionComponent: Component { } } - func update(component: StoryContentCaptionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryContentCaptionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -652,46 +667,41 @@ final class StoryContentCaptionComponent: Component { cachedMessageSyntaxHighlight: self.codeHighlight ) - let truncationToken = NSMutableAttributedString() - truncationToken.append(NSAttributedString(string: "\u{2026} ", font: Font.regular(16.0), textColor: .white)) - truncationToken.append(NSAttributedString(string: component.strings.Story_CaptionShowMore, font: Font.semibold(16.0), textColor: .white)) + let truncationTokenString = component.strings.Story_CaptionShowMore + let customTruncationToken: (UIFont, Bool) -> NSAttributedString? = { baseFont, _ in + let truncationToken = NSMutableAttributedString() + truncationToken.append(NSAttributedString(string: "\u{2026} ", font: Font.regular(baseFont.pointSize), textColor: .white)) + truncationToken.append(NSAttributedString(string: truncationTokenString, font: Font.semibold(baseFont.pointSize), textColor: .white)) + return truncationToken + } - let collapsedTextLayout = TextNodeWithEntities.asyncLayout(self.collapsedText.textNode)(TextNodeLayoutArguments( + let textInsets = UIEdgeInsets(top: 2.0, left: 2.0, bottom: 5.0, right: 2.0) + let collapsedTextLayout = InteractiveTextNodeWithEntities.asyncLayout(self.collapsedText.textNode)(InteractiveTextNodeLayoutArguments( attributedString: attributedText, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: textContainerSize.width, height: 10000.0), + insets: textInsets, textShadowColor: UIColor(white: 0.0, alpha: 0.25), textShadowBlur: 4.0, - displaySpoilers: false, - customTruncationToken: truncationToken + displayContentsUnderSpoilers: self.displayContentsUnderSpoilers.value, + customTruncationToken: customTruncationToken, + expandedBlocks: self.expandedContentsBlocks )) - let expandedTextLayout = TextNodeWithEntities.asyncLayout(self.expandedText.textNode)(TextNodeLayoutArguments( + let expandedTextLayout = InteractiveTextNodeWithEntities.asyncLayout(self.expandedText.textNode)(InteractiveTextNodeLayoutArguments( attributedString: attributedText, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: textContainerSize.width, height: 10000.0), + insets: textInsets, textShadowColor: UIColor(white: 0.0, alpha: 0.25), textShadowBlur: 4.0, - displaySpoilers: false + displayContentsUnderSpoilers: self.displayContentsUnderSpoilers.value, + expandedBlocks: self.expandedContentsBlocks )) - let collapsedSpoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? - if !collapsedTextLayout.0.spoilers.isEmpty { - collapsedSpoilerTextLayoutAndApply = TextNodeWithEntities.asyncLayout(self.collapsedText.spoilerTextNode)(TextNodeLayoutArguments(attributedString: attributedText, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: textContainerSize.width, height: 10000.0), textShadowColor: UIColor(white: 0.0, alpha: 0.25), textShadowBlur: 4.0, displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true)) - } else { - collapsedSpoilerTextLayoutAndApply = nil - } - - let expandedSpoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? - if !expandedTextLayout.0.spoilers.isEmpty { - expandedSpoilerTextLayoutAndApply = TextNodeWithEntities.asyncLayout(self.expandedText.spoilerTextNode)(TextNodeLayoutArguments(attributedString: attributedText, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: textContainerSize.width, height: 10000.0), textShadowColor: UIColor(white: 0.0, alpha: 0.25), textShadowBlur: 4.0, displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true)) - } else { - expandedSpoilerTextLayoutAndApply = nil - } - - let visibleTextHeight = collapsedTextLayout.0.size.height - let textOverflowHeight: CGFloat = expandedTextLayout.0.size.height - visibleTextHeight + let visibleTextHeight = collapsedTextLayout.0.size.height - textInsets.top - textInsets.bottom + let textOverflowHeight: CGFloat = expandedTextLayout.0.size.height - textInsets.top - textInsets.bottom - visibleTextHeight let scrollContentSize = CGSize(width: availableSize.width, height: availableSize.height + textOverflowHeight) if let forwardInfo = component.forwardInfo { @@ -789,14 +799,55 @@ final class StoryContentCaptionComponent: Component { forwardInfoPanel.view?.removeFromSuperview() } + + let collapsedTextFrame = CGRect(origin: CGPoint(x: sideInset - textInsets.left, y: availableSize.height - visibleTextHeight - verticalInset - textInsets.top), size: collapsedTextLayout.0.size) + let expandedTextFrame = CGRect(origin: CGPoint(x: sideInset - textInsets.left, y: availableSize.height - visibleTextHeight - verticalInset - textInsets.top), size: expandedTextLayout.0.size) + + var spoilerExpandRect: CGRect? + if let location = self.displayContentsUnderSpoilers.location { + self.displayContentsUnderSpoilers.location = nil + + let mappedLocation = CGPoint(x: location.x, y: location.y) + + let getDistance: (CGPoint, CGPoint) -> CGFloat = { a, b in + let v = CGPoint(x: a.x - b.x, y: a.y - b.y) + return sqrt(v.x * v.x + v.y * v.y) + } + + var maxDistance: CGFloat = getDistance(mappedLocation, CGPoint(x: 0.0, y: 0.0)) + maxDistance = max(maxDistance, getDistance(mappedLocation, CGPoint(x: expandedTextFrame.width, y: 0.0))) + maxDistance = max(maxDistance, getDistance(mappedLocation, CGPoint(x: expandedTextFrame.width, y: expandedTextFrame.height))) + maxDistance = max(maxDistance, getDistance(mappedLocation, CGPoint(x: 0.0, y: expandedTextFrame.height))) + + let mappedSize = CGSize(width: maxDistance * 2.0, height: maxDistance * 2.0) + spoilerExpandRect = mappedSize.centered(around: mappedLocation) + } + + let textAnimation: ListViewItemUpdateAnimation + if case let .curve(duration, curve) = transition.animation { + textAnimation = .System(duration: duration, transition: ControlledTransition(duration: duration, curve: curve.containedViewLayoutTransitionCurve, interactive: false)) + } else { + textAnimation = .None + } + let textApplyArguments = InteractiveTextNodeWithEntities.Arguments( + context: component.context, + cache: component.context.animationCache, + renderer: component.context.animationRenderer, + placeholderColor: UIColor(white: 0.2, alpha: 1.0), + attemptSynchronous: true, + textColor: .white, + spoilerEffectColor: .white, + applyArguments: InteractiveTextNode.ApplyArguments( + animation: textAnimation, + spoilerTextColor: .white, + spoilerEffectColor: .white, + areContentAnimationsEnabled: true, + spoilerExpandRect: spoilerExpandRect + ) + ) + do { - let collapsedTextNode = collapsedTextLayout.1(TextNodeWithEntities.Arguments( - context: component.context, - cache: component.context.animationCache, - renderer: component.context.animationRenderer, - placeholderColor: UIColor(white: 0.2, alpha: 1.0), - attemptSynchronous: true - )) + let collapsedTextNode = collapsedTextLayout.1(textApplyArguments) if self.collapsedText.textNode !== collapsedTextNode { self.collapsedText.textNode?.textNode.view.removeFromSuperview() @@ -810,61 +861,11 @@ final class StoryContentCaptionComponent: Component { collapsedTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0)) } - let collapsedTextFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - visibleTextHeight - verticalInset), size: collapsedTextLayout.0.size) collapsedTextNode.textNode.frame = collapsedTextFrame - - if let (_, collapsedSpoilerTextApply) = collapsedSpoilerTextLayoutAndApply { - let collapsedSpoilerTextNode = collapsedSpoilerTextApply(TextNodeWithEntities.Arguments( - context: component.context, - cache: component.context.animationCache, - renderer: component.context.animationRenderer, - placeholderColor: UIColor(white: 0.2, alpha: 1.0), - attemptSynchronous: true - )) - if self.collapsedText.spoilerTextNode == nil { - collapsedSpoilerTextNode.textNode.alpha = 0.0 - collapsedSpoilerTextNode.textNode.isUserInteractionEnabled = false - collapsedSpoilerTextNode.textNode.contentMode = .topLeft - collapsedSpoilerTextNode.textNode.contentsScale = UIScreenScale - collapsedSpoilerTextNode.textNode.displaysAsynchronously = false - self.collapsedText.insertSubview(collapsedSpoilerTextNode.textNode.view, belowSubview: collapsedTextNode.textNode.view) - - collapsedSpoilerTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0)) - - self.collapsedText.spoilerTextNode = collapsedSpoilerTextNode - } - - self.collapsedText.spoilerTextNode?.textNode.frame = collapsedTextFrame - - let collapsedDustNode: InvisibleInkDustNode - if let current = self.collapsedText.dustNode { - collapsedDustNode = current - } else { - collapsedDustNode = InvisibleInkDustNode(textNode: collapsedSpoilerTextNode.textNode, enableAnimations: component.context.sharedContext.energyUsageSettings.fullTranslucency) - self.collapsedText.dustNode = collapsedDustNode - self.collapsedText.insertSubview(collapsedDustNode.view, aboveSubview: collapsedSpoilerTextNode.textNode.view) - } - collapsedDustNode.frame = collapsedTextFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 0.0) - collapsedDustNode.update(size: collapsedDustNode.frame.size, color: .white, textColor: .white, rects: collapsedTextLayout.0.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: collapsedTextLayout.0.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) - } else if let collapsedSpoilerTextNode = self.collapsedText.spoilerTextNode { - self.collapsedText.spoilerTextNode = nil - collapsedSpoilerTextNode.textNode.removeFromSupernode() - - if let collapsedDustNode = self.collapsedText.dustNode { - self.collapsedText.dustNode = nil - collapsedDustNode.view.removeFromSuperview() - } - } } do { - let expandedTextNode = expandedTextLayout.1(TextNodeWithEntities.Arguments( - context: component.context, - cache: component.context.animationCache, - renderer: component.context.animationRenderer, - placeholderColor: UIColor(white: 0.2, alpha: 1.0), - attemptSynchronous: true - )) + let expandedTextNode = expandedTextLayout.1(textApplyArguments) if self.expandedText.textNode !== expandedTextNode { self.expandedText.textNode?.textNode.view.removeFromSuperview() @@ -876,51 +877,7 @@ final class StoryContentCaptionComponent: Component { expandedTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0)) } - let expandedTextFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - visibleTextHeight - verticalInset), size: expandedTextLayout.0.size) expandedTextNode.textNode.frame = expandedTextFrame - - if let (_, expandedSpoilerTextApply) = expandedSpoilerTextLayoutAndApply { - let expandedSpoilerTextNode = expandedSpoilerTextApply(TextNodeWithEntities.Arguments( - context: component.context, - cache: component.context.animationCache, - renderer: component.context.animationRenderer, - placeholderColor: UIColor(white: 0.2, alpha: 1.0), - attemptSynchronous: true - )) - if self.expandedText.spoilerTextNode == nil { - expandedSpoilerTextNode.textNode.alpha = 0.0 - expandedSpoilerTextNode.textNode.isUserInteractionEnabled = false - expandedSpoilerTextNode.textNode.contentMode = .topLeft - expandedSpoilerTextNode.textNode.contentsScale = UIScreenScale - expandedSpoilerTextNode.textNode.displaysAsynchronously = false - self.expandedText.insertSubview(expandedSpoilerTextNode.textNode.view, belowSubview: expandedTextNode.textNode.view) - - expandedSpoilerTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0)) - - self.expandedText.spoilerTextNode = expandedSpoilerTextNode - } - - self.expandedText.spoilerTextNode?.textNode.frame = expandedTextFrame - - let expandedDustNode: InvisibleInkDustNode - if let current = self.expandedText.dustNode { - expandedDustNode = current - } else { - expandedDustNode = InvisibleInkDustNode(textNode: expandedSpoilerTextNode.textNode, enableAnimations: component.context.sharedContext.energyUsageSettings.fullTranslucency) - self.expandedText.dustNode = expandedDustNode - self.expandedText.insertSubview(expandedDustNode.view, aboveSubview: expandedSpoilerTextNode.textNode.view) - } - expandedDustNode.frame = expandedTextFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 0.0) - expandedDustNode.update(size: expandedDustNode.frame.size, color: .white, textColor: .white, rects: expandedTextLayout.0.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: expandedTextLayout.0.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) - } else if let expandedSpoilerTextNode = self.expandedText.spoilerTextNode { - self.expandedText.spoilerTextNode = nil - expandedSpoilerTextNode.textNode.removeFromSupernode() - - if let expandedDustNode = self.expandedText.dustNode { - self.expandedText.dustNode = nil - expandedDustNode.view.removeFromSuperview() - } - } } if self.textSelectionNode == nil, let controller = component.controller(), let textNode = self.expandedText.textNode?.textNode { @@ -956,16 +913,6 @@ final class StoryContentCaptionComponent: Component { } component.textSelectionAction(text, action) }) - /*textSelectionNode.updateRange = { [weak self] selectionRange in - if let strongSelf = self, let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.textNode.cachedLayout, !textLayout.spoilers.isEmpty, let selectionRange = selectionRange { - for (spoilerRange, _) in textLayout.spoilers { - if let intersection = selectionRange.intersection(spoilerRange), intersection.length > 0 { - dustNode.update(revealed: true) - return - } - } - } - }*/ textSelectionNode.enableLookup = true self.textSelectionNode = textSelectionNode self.scrollView.addSubview(textSelectionNode.view) @@ -985,7 +932,7 @@ final class StoryContentCaptionComponent: Component { if let (index, attributes) = textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) { let action: Action? - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(contentItem.dustNode?.isRevealed ?? true) { + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !self.displayContentsUnderSpoilers.value { return false } else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { var concealed = true @@ -1014,15 +961,9 @@ final class StoryContentCaptionComponent: Component { return true } - //let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) - //textSelectionNode.view.addGestureRecognizer(tapRecognizer) - let _ = textSelectionNode.view let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) - /*if let selectionRecognizer = textSelectionNode.recognizer { - recognizer.require(toFail: selectionRecognizer) - }*/ recognizer.tapActionAtPoint = { point in return .waitForSingleTap } @@ -1050,6 +991,7 @@ final class StoryContentCaptionComponent: Component { ) self.ignoreScrolling = true + let previousBounds = self.scrollView.bounds if self.scrollView.contentSize != scrollContentSize { self.scrollView.contentSize = scrollContentSize @@ -1057,27 +999,13 @@ final class StoryContentCaptionComponent: Component { transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) transition.setFrame(view: self.scrollViewContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) - /*if self.shadowGradientLayer.colors == nil { - var locations: [NSNumber] = [] - var colors: [CGColor] = [] - let numStops = 10 - let baseAlpha: CGFloat = 0.6 - for i in 0 ..< numStops { - let step = 1.0 - CGFloat(i) / CGFloat(numStops - 1) - locations.append((1.0 - step) as NSNumber) - let alphaStep: CGFloat = pow(step, 1.0) - colors.append(UIColor.black.withAlphaComponent(alphaStep * baseAlpha).cgColor) + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) } - - self.shadowGradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0) - self.shadowGradientLayer.endPoint = CGPoint(x: 0.0, y: 0.0) - - self.shadowGradientLayer.locations = locations - self.shadowGradientLayer.colors = colors - self.shadowGradientLayer.type = .axial - - self.shadowPlainLayer.backgroundColor = UIColor(white: 0.0, alpha: baseAlpha).cgColor - }*/ + } self.ignoreScrolling = false self.updateScrolling(transition: transition) @@ -1100,33 +1028,6 @@ final class StoryContentCaptionComponent: Component { isExpandedTransition.setAlpha(view: self.collapsedText, alpha: self.isExpanded ? 0.0 : 1.0) isExpandedTransition.setAlpha(view: self.expandedText, alpha: !self.isExpanded ? 0.0 : 1.0) - /*if let spoilerTextNode = self.collapsedText.spoilerTextNode { - var spoilerAlpha = self.isExpanded ? 0.0 : 1.0 - if let dustNode = self.collapsedText.dustNode, dustNode.isRevealed { - } else { - spoilerAlpha = 0.0 - } - isExpandedTransition.setAlpha(view: spoilerTextNode.textNode.view, alpha: spoilerAlpha) - } - if let dustNode = self.collapsedText.dustNode { - isExpandedTransition.setAlpha(view: dustNode.view, alpha: self.isExpanded ? 0.0 : 1.0) - }*/ - - /*if let textNode = self.expandedText.textNode { - isExpandedTransition.setAlpha(view: textNode.textNode.view, alpha: !self.isExpanded ? 0.0 : 1.0) - } - if let spoilerTextNode = self.expandedText.spoilerTextNode { - var spoilerAlpha = !self.isExpanded ? 0.0 : 1.0 - if let dustNode = self.expandedText.dustNode, dustNode.isRevealed { - } else { - spoilerAlpha = 0.0 - } - isExpandedTransition.setAlpha(view: spoilerTextNode.textNode.view, alpha: spoilerAlpha) - } - if let dustNode = self.expandedText.dustNode { - isExpandedTransition.setAlpha(view: dustNode.view, alpha: !self.isExpanded ? 0.0 : 1.0) - }*/ - isExpandedTransition.setAlpha(view: self.shadowGradientView, alpha: self.isExpanded ? 0.0 : 1.0) isExpandedTransition.setAlpha(view: self.scrollBottomMaskView, alpha: self.isExpanded ? 1.0 : 0.0) @@ -1140,7 +1041,7 @@ final class StoryContentCaptionComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryInteractionGuideComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryInteractionGuideComponent.swift index 62b24ccc464..8b0fcd0642d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryInteractionGuideComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryInteractionGuideComponent.swift @@ -91,7 +91,7 @@ final class StoryInteractionGuideComponent: Component { self.containerView.layer.animateScale(from: 1.0, to: 1.1, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) } - func update(component: StoryInteractionGuideComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryInteractionGuideComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -261,7 +261,7 @@ final class StoryInteractionGuideComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -337,7 +337,7 @@ private final class GuideItemComponent: Component { } private var isPlaying = false - func update(component: GuideItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: GuideItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let originX = availableSize.width / 2.0 - 120.0 @@ -444,7 +444,7 @@ private final class GuideItemComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 2fab648f95d..fd2bef30985 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -547,7 +547,7 @@ final class StoryItemContentComponent: Component { return nil } - private func updateOverlays(component: StoryItemContentComponent, size: CGSize, synchronousLoad: Bool, transition: Transition) { + private func updateOverlays(component: StoryItemContentComponent, size: CGSize, synchronousLoad: Bool, transition: ComponentTransition) { self.overlaysView.update( context: component.context, strings: component.strings, @@ -579,7 +579,7 @@ final class StoryItemContentComponent: Component { self.isSeeking = false } - func update(component: StoryItemContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryItemContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousItem = self.component?.item self.component = component @@ -917,7 +917,18 @@ final class StoryItemContentComponent: Component { mediaAreasEffectView.removeFromSuperview() } } - if !component.item.mediaAreas.isEmpty { + + let shimmeringMediaAreas: [MediaArea] = component.item.mediaAreas.filter { mediaArea in + if case .link = mediaArea { + return true + } else if case .venue = mediaArea { + return true + } else { + return false + } + } + + if !shimmeringMediaAreas.isEmpty { let mediaAreasEffectView: StoryItemLoadingEffectView if let current = self.mediaAreasEffectView { mediaAreasEffectView = current @@ -928,44 +939,14 @@ final class StoryItemContentComponent: Component { } mediaAreasEffectView.update(size: availableSize, transition: transition) - let maskLayer: CALayer - if let current = mediaAreasEffectView.layer.mask { + let maskLayer: MediaAreaMaskLayer + if let current = mediaAreasEffectView.layer.mask as? MediaAreaMaskLayer { maskLayer = current } else { - maskLayer = CALayer() + maskLayer = MediaAreaMaskLayer() mediaAreasEffectView.layer.mask = maskLayer } - - if (maskLayer.sublayers ?? []).isEmpty { - let referenceSize = availableSize - for mediaArea in component.item.mediaAreas { - guard case .venue = mediaArea else { - continue - } - let size = CGSize(width: mediaArea.coordinates.width / 100.0 * referenceSize.width, height: mediaArea.coordinates.height / 100.0 * referenceSize.height) - let position = CGPoint(x: mediaArea.coordinates.x / 100.0 * referenceSize.width, y: mediaArea.coordinates.y / 100.0 * referenceSize.height) - let cornerRadius = size.height * 0.18 - - let layer = CALayer() - layer.backgroundColor = UIColor.white.cgColor - layer.bounds = CGRect(origin: .zero, size: size) - layer.position = position - layer.cornerRadius = cornerRadius - maskLayer.addSublayer(layer) - - let borderLayer = CAShapeLayer() - borderLayer.strokeColor = UIColor.white.cgColor - borderLayer.fillColor = UIColor.clear.cgColor - borderLayer.lineWidth = 2.0 - borderLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: size), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil) - borderLayer.bounds = CGRect(origin: .zero, size: size) - borderLayer.position = position - mediaAreasEffectView.borderMaskLayer.addSublayer(borderLayer) - - layer.transform = CATransform3DMakeRotation(mediaArea.coordinates.rotation * Double.pi / 180.0, 0.0, 0.0, 1.0) - borderLayer.transform = layer.transform - } - } + maskLayer.update(referenceSize: availableSize, mediaAreas: shimmeringMediaAreas, borderMaskLayer: mediaAreasEffectView.borderMaskLayer) } else if let mediaAreasEffectView = self.mediaAreasEffectView { self.mediaAreasEffectView = nil mediaAreasEffectView.removeFromSuperview() @@ -980,7 +961,7 @@ final class StoryItemContentComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift index 6200db98127..6e23b837d04 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift @@ -66,7 +66,7 @@ final class StoryItemImageView: UIView { } } - func update(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, storyId: Int32, media: EngineMedia, size: CGSize, isCaptureProtected: Bool, attemptSynchronous: Bool, transition: Transition) { + func update(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, storyId: Int32, media: EngineMedia, size: CGSize, isCaptureProtected: Bool, attemptSynchronous: Bool, transition: ComponentTransition) { self.backgroundColor = isCaptureProtected ? UIColor(rgb: 0x181818) : nil var dimensions: CGSize? @@ -296,7 +296,7 @@ final class CaptureProtectedInfoComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: CaptureProtectedInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: CaptureProtectedInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let iconSize = self.icon.update( transition: transition, component: AnyComponent(BundleIconComponent( @@ -367,7 +367,7 @@ final class CaptureProtectedInfoComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift index 07a8ef0dab0..81f7a6d9d0e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift @@ -102,7 +102,7 @@ final class StoryItemLoadingEffectView: UIView { self.didPlayOnce = true } - func update(size: CGSize, transition: Transition) { + func update(size: CGSize, transition: ComponentTransition) { if self.backgroundView.bounds.size != size { self.backgroundView.layer.removeAllAnimations() diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift index 7f732455a01..7f7863caf77 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift @@ -262,10 +262,10 @@ final class StoryItemOverlaysView: UIView { } if highlighted { - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) transition.setSublayerTransform(view: self, transform: CATransform3DMakeScale(0.9, 0.9, 1.0)) } else { - let transition: Transition = .immediate + let transition: ComponentTransition = .immediate transition.setSublayerTransform(view: self, transform: CATransform3DIdentity) var fromScale: Double = 0.9 if self.layer.animation(forKey: "sublayerTransform") != nil, let presentation = self.layer.presentation() { @@ -305,7 +305,7 @@ final class StoryItemOverlaysView: UIView { size: CGSize, isActive: Bool ) { - var transition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut)) + var transition = ComponentTransition(animation: .curve(duration: 0.18, curve: .easeInOut)) if self.reaction == nil { transition = .immediate } @@ -559,7 +559,7 @@ final class StoryItemOverlaysView: UIView { isCaptureProtected: Bool, attemptSynchronous: Bool, isActive: Bool, - transition: Transition + transition: ComponentTransition ) { var nextId = 0 for mediaArea in story.mediaAreas { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 5ebfa323368..8f416abce8f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -77,7 +77,7 @@ public final class StoryItemSetContainerComponent: Component { public enum NavigationDirection { case previous case next - case id(Int32) + case id(StoryId) } public struct PinchState: Equatable { @@ -335,11 +335,11 @@ public final class StoryItemSetContainerComponent: Component { } final class CaptionItem { - let itemId: Int32 + let itemId: StoryId let externalState = StoryContentCaptionComponent.ExternalState() let view = ComponentView() - init(itemId: Int32) { + init(itemId: StoryId) { self.itemId = itemId } } @@ -442,7 +442,7 @@ public final class StoryItemSetContainerComponent: Component { var isSearchActive: Bool = false - var viewLists: [Int32: ViewList] = [:] + var viewLists: [StoryId: ViewList] = [:] let viewListsContainer: UIView var isEditingStory: Bool = false @@ -450,8 +450,8 @@ public final class StoryItemSetContainerComponent: Component { var itemLayout: ItemLayout? var ignoreScrolling: Bool = false - var visibleItems: [Int32: VisibleItem] = [:] - var trulyValidIds: [Int32] = [] + var visibleItems: [StoryId: VisibleItem] = [:] + var trulyValidIds: [StoryId] = [] var reactionContextNode: ReactionContextNode? weak var disappearingReactionContextNode: ReactionContextNode? @@ -477,8 +477,8 @@ public final class StoryItemSetContainerComponent: Component { let transitionCloneContainerView: UIView - private var awaitingSwitchToId: (from: Int32, to: Int32)? - private var animateNextNavigationId: Int32? + private var awaitingSwitchToId: (from: StoryId, to: StoryId)? + private var animateNextNavigationId: StoryId? private var initializedOffset: Bool = false private var viewListPanState: PanState? @@ -647,7 +647,7 @@ public final class StoryItemSetContainerComponent: Component { }) } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } }) @@ -690,7 +690,7 @@ public final class StoryItemSetContainerComponent: Component { return } self.sendMessageContext.hasRecordedVideoPreview = true - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } self.component?.controller()?.present(videoRecorder, in: .window(.root)) @@ -703,7 +703,7 @@ public final class StoryItemSetContainerComponent: Component { let _ = previousVideoRecorderValue.dismissVideo() } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } }) } @@ -731,7 +731,7 @@ public final class StoryItemSetContainerComponent: Component { guard let component = self.component else { return false } - guard let visibleItem = self.visibleItems[component.slice.item.storyItem.id] else { + guard let visibleItem = self.visibleItems[component.slice.item.id] else { return false } guard let itemView = visibleItem.view.view as? StoryItemContentComponent.View else { @@ -825,7 +825,7 @@ public final class StoryItemSetContainerComponent: Component { guard let component = self.component else { return } - guard let visibleItem = self.visibleItems[component.slice.item.storyItem.id] else { + guard let visibleItem = self.visibleItems[component.slice.item.id] else { return } if let itemView = visibleItem.view.view as? StoryContentItem.View { @@ -837,7 +837,7 @@ public final class StoryItemSetContainerComponent: Component { guard let component = self.component else { return } - guard let visibleItem = self.visibleItems[component.slice.item.storyItem.id] else { + guard let visibleItem = self.visibleItems[component.slice.item.id] else { return } if let itemView = visibleItem.view.view as? StoryContentItem.View { @@ -851,7 +851,7 @@ public final class StoryItemSetContainerComponent: Component { guard let component = self.component else { return } - guard let visibleItem = self.visibleItems[component.slice.item.storyItem.id] else { + guard let visibleItem = self.visibleItems[component.slice.item.id] else { return } if let itemView = visibleItem.view.view as? StoryContentItem.View { @@ -878,7 +878,7 @@ public final class StoryItemSetContainerComponent: Component { if otherGestureRecognizer.view is UIScrollView { return true } - if let component = self.component, let viewList = self.viewLists[component.slice.item.storyItem.id], let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View { + if let component = self.component, let viewList = self.viewLists[component.slice.item.id], let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View { if otherGestureRecognizer.view === viewListView { return true } @@ -913,9 +913,6 @@ public final class StoryItemSetContainerComponent: Component { @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state, let component = self.component, let itemLayout = self.itemLayout { - if let _ = self.sendMessageContext.menuController { - return - } if self.displayLikeReactions { self.displayLikeReactions = false self.sendMessageContext.currentInputMode = .text @@ -925,7 +922,7 @@ public final class StoryItemSetContainerComponent: Component { self.endEditing(true) } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))) self.updateIsProgressPaused() } else if self.hasActiveDeactivateableInput() { Queue.mainQueue().justDispatch { @@ -936,8 +933,8 @@ public final class StoryItemSetContainerComponent: Component { for (id, visibleItem) in self.visibleItems { if visibleItem.contentContainerView.convert(visibleItem.contentContainerView.bounds, to: self).contains(point) { - if id == component.slice.item.storyItem.id { - let transition = Transition(animation: .curve(duration: 0.4, curve: .spring)) + if id == component.slice.item.id { + let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)) self.viewListDisplayState = .hidden self.isSearchActive = false @@ -956,7 +953,7 @@ public final class StoryItemSetContainerComponent: Component { if captionItem.externalState.isSelectingText { captionItemView.cancelTextSelection() } else { - captionItemView.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + captionItemView.collapse(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } } } else { @@ -1035,22 +1032,22 @@ public final class StoryItemSetContainerComponent: Component { let velocity = recognizer.velocity(in: self) var consumed = false - if let component = self.component, let currentIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) { + if let component = self.component, let currentIndex = component.slice.allItems.firstIndex(where: { $0.id == component.slice.item.id }) { if (viewListPanState.fraction <= -0.3 || (viewListPanState.fraction <= -0.05 && velocity.x <= -200.0)), currentIndex != component.slice.allItems.count - 1 { let nextItem = component.slice.allItems[currentIndex + 1] - self.animateNextNavigationId = nextItem.storyItem.id - component.navigate(.id(nextItem.storyItem.id)) + self.animateNextNavigationId = nextItem.id + component.navigate(.id(nextItem.id)) consumed = true } else if (viewListPanState.fraction >= 0.3 || (viewListPanState.fraction >= 0.05 && velocity.x >= 200.0)), currentIndex != 0 { let previousItem = component.slice.allItems[currentIndex - 1] - self.animateNextNavigationId = previousItem.storyItem.id - component.navigate(.id(previousItem.storyItem.id)) + self.animateNextNavigationId = previousItem.id + component.navigate(.id(previousItem.id)) consumed = true } } if !consumed { - let transition = Transition(animation: .curve(duration: 0.4, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)) self.viewListPanState = nil self.isCompletingViewListPan = true transition.attachAnimation(view: self, id: "isCompletingViewListPan", completion: { [weak self] completed in @@ -1083,7 +1080,7 @@ public final class StoryItemSetContainerComponent: Component { verticalPanState.fraction = fraction } else { var targetScrollView: UIScrollView? - if case .began = recognizer.state, self.viewListDisplayState != .hidden, let viewList = self.viewLists[component.slice.item.storyItem.id], let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View { + if case .began = recognizer.state, self.viewListDisplayState != .hidden, let viewList = self.viewLists[component.slice.item.id], let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View { if let hitResult = viewListView.hitTest(self.convert(recognizer.location(in: self), to: viewListView), with: nil) { func findTargetScrollView(target: UIView, minParent: UIView) -> UIScrollView? { if target === viewListView { @@ -1148,7 +1145,7 @@ public final class StoryItemSetContainerComponent: Component { if verticalPanState.accumulatedOffset > 0.0 || resetContentOffset { scrollView.contentOffset = CGPoint() - if self.viewListDisplayState != .hidden, let viewList = self.viewLists[component.slice.item.storyItem.id], let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View { + if self.viewListDisplayState != .hidden, let viewList = self.viewLists[component.slice.item.id], let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View { let eventCycleState = StoryItemSetViewListComponent.EventCycleState() eventCycleState.ignoreScrolling = true viewListView.setEventCycleState(eventCycleState) @@ -1196,16 +1193,16 @@ public final class StoryItemSetContainerComponent: Component { } } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } else if verticalPanState.accumulatedOffset < 0.0 && self.targetViewListDisplayStateIsFull { if verticalPanState.fraction <= -0.05 || velocity.y <= -80.0 { self.viewListDisplayState = .full } else { self.viewListDisplayState = .half } - self.state?.updated(transition: verticalPanState.accumulatedOffset == 0.0 ? .immediate : Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: verticalPanState.accumulatedOffset == 0.0 ? .immediate : ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } else { - self.state?.updated(transition: verticalPanState.accumulatedOffset == 0.0 ? .immediate : Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: verticalPanState.accumulatedOffset == 0.0 ? .immediate : ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } } else { if verticalPanState.fraction >= 0.3 || (verticalPanState.fraction >= 0.05 && velocity.y >= 150.0) { @@ -1220,36 +1217,36 @@ public final class StoryItemSetContainerComponent: Component { self.dismissAllTooltips() } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } } else { - if let visibleItemView = self.visibleItems[component.slice.item.storyItem.id]?.view.view as? StoryItemContentComponent.View { + if let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View { visibleItemView.seekEnded() } if translation.y > 200.0 || (translation.y > 5.0 && velocity.y > 200.0) { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) self.component?.controller()?.dismiss() } else if translation.y < -200.0 || (translation.y < -100.0 && velocity.y < -100.0) { var displayViewLists = false - if component.slice.peer.id == component.context.account.peerId { + if component.slice.effectivePeer.id == component.context.account.peerId { displayViewLists = true - } else if case let .channel(channel) = component.slice.peer, channel.flags.contains(.isCreator) || component.slice.additionalPeerData.canViewStats { + } else if case let .channel(channel) = component.slice.effectivePeer, channel.flags.contains(.isCreator) || component.slice.additionalPeerData.canViewStats { displayViewLists = true } if displayViewLists { self.viewListDisplayState = self.targetViewListDisplayStateIsFull ? .full : .half - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) self.dismissAllTooltips() } else { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) if let activate = self.activateInputWhileDragging() { activate() } } } else { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } } } @@ -1266,7 +1263,7 @@ public final class StoryItemSetContainerComponent: Component { if self.viewListDisplayState != .hidden { self.viewListDisplayState = .hidden self.isSearchActive = false - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } else { component.close() } @@ -1311,10 +1308,9 @@ public final class StoryItemSetContainerComponent: Component { var index = Int(round(scrollView.contentOffset.x / itemLayout.fullItemScrollDistance)) index = max(0, min(index, component.slice.allItems.count - 1)) - if let currentIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) { + if let currentIndex = component.slice.allItems.firstIndex(where: { $0.id == component.slice.item.id }) { if index != currentIndex { - let nextId = component.slice.allItems[index].storyItem.id - //self.awaitingSwitchToId = (component.slice.allItems[currentIndex].storyItem.id, nextId) + let nextId = component.slice.allItems[index].id component.navigate(.id(nextId)) } } @@ -1419,7 +1415,7 @@ public final class StoryItemSetContainerComponent: Component { return .play } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let itemLayout = self.itemLayout else { return } @@ -1429,8 +1425,8 @@ public final class StoryItemSetContainerComponent: Component { hintAllowSynchronousLoads = hint.allowSynchronousLoads } - var validIds: [Int32] = [] - var trulyValidIds: [Int32] = [] + var validIds: [StoryId] = [] + var trulyValidIds: [StoryId] = [] let centralItemX = itemLayout.contentFrame.center.x @@ -1440,7 +1436,7 @@ public final class StoryItemSetContainerComponent: Component { let scaledFullItemScrollDistance = scaledCentralVisibleItemWidth * 0.5 + itemLayout.itemSpacing + scaledSideVisibleItemWidth * 0.5 let scaledHalfItemScrollDistance = scaledSideVisibleItemWidth * 0.5 + itemLayout.itemSpacing + scaledSideVisibleItemWidth * 0.5 - if let centralIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) { + if let centralIndex = component.slice.allItems.firstIndex(where: { $0.id == component.slice.item.id }) { let centralItemOffset: CGFloat = itemLayout.fullItemScrollDistance * CGFloat(centralIndex) let effectiveScrollingOffsetX = self.scroller.contentOffset.x * itemLayout.contentScaleFraction + centralItemOffset * (1.0 - itemLayout.contentScaleFraction) @@ -1487,7 +1483,7 @@ public final class StoryItemSetContainerComponent: Component { if transition.animation.isImmediate { continue } else { - if self.visibleItems[item.storyItem.id] == nil { + if self.visibleItems[item.id] == nil { continue } else { reevaluateVisibilityOnCompletion = true @@ -1500,19 +1496,19 @@ public final class StoryItemSetContainerComponent: Component { let minItemScale = itemLayout.contentMinScale * (1.0 - scaleFraction) + itemLayout.sideVisibleItemScale * scaleFraction let itemScale: CGFloat = itemLayout.contentScaleFraction * minItemScale + (1.0 - itemLayout.contentScaleFraction) * 1.0 - validIds.append(item.storyItem.id) + validIds.append(item.id) if itemVisible { - trulyValidIds.append(item.storyItem.id) + trulyValidIds.append(item.id) } var itemTransition = transition let visibleItem: VisibleItem - if let current = self.visibleItems[item.storyItem.id] { + if let current = self.visibleItems[item.id] { visibleItem = current } else { itemTransition = .immediate visibleItem = VisibleItem() - self.visibleItems[item.storyItem.id] = visibleItem + self.visibleItems[item.id] = visibleItem } let itemEnvironment = StoryContentItem.Environment( @@ -1563,7 +1559,7 @@ public final class StoryItemSetContainerComponent: Component { component: AnyComponent(StoryItemContentComponent( context: component.context, strings: component.strings, - peer: component.slice.peer, + peer: component.slice.effectivePeer, item: item.storyItem, availableReactions: component.availableReactions, entityFiles: item.entityFiles, @@ -1595,7 +1591,7 @@ public final class StoryItemSetContainerComponent: Component { itemTransition.setPosition(view: view, position: CGPoint(x: itemLayout.contentFrame.size.width * 0.5, y: itemLayout.contentFrame.size.height * 0.5)) itemTransition.setBounds(view: view, bounds: CGRect(origin: CGPoint(), size: itemLayout.contentFrame.size)) - let itemId = item.storyItem.id + let itemId = item.id itemTransition.setPosition(view: visibleItem.contentContainerView, position: CGPoint(x: itemPositionX, y: itemLayout.contentFrame.center.y), completion: { [weak self] _ in guard reevaluateVisibilityOnCompletion, let self else { return @@ -1661,7 +1657,7 @@ public final class StoryItemSetContainerComponent: Component { var isChannel = false var canShare = true var displayFooter = false - if case let .channel(channel) = component.slice.peer { + if case let .channel(channel) = component.slice.effectivePeer { isChannel = true if channel.addressName == nil { canShare = false @@ -1678,7 +1674,7 @@ public final class StoryItemSetContainerComponent: Component { displayFooter = true } } - } else if component.slice.peer.id == component.context.account.peerId { + } else if component.slice.effectivePeer.id == component.context.account.peerId { displayFooter = true } else if component.slice.item.storyItem.isPending { displayFooter = true @@ -1723,7 +1719,7 @@ public final class StoryItemSetContainerComponent: Component { context: component.context, theme: component.theme, strings: component.strings, - peer: component.slice.peer, + peer: component.slice.effectivePeer, storyItem: item.storyItem, myReaction: item.storyItem.myReaction.flatMap { value -> StoryFooterPanelComponent.MyReaction? in var centerAnimation: TelegramMediaFile? @@ -1758,7 +1754,7 @@ public final class StoryItemSetContainerComponent: Component { guard let self, let component = self.component else { return } - if self.viewLists[component.slice.item.storyItem.id] == nil { + if self.viewLists[component.slice.item.id] == nil { return } @@ -1769,7 +1765,7 @@ public final class StoryItemSetContainerComponent: Component { self.updateScrolling(transition: .immediate) self.preparingToDisplayViewList = false - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) self.dismissAllTooltips() } @@ -1889,7 +1885,7 @@ public final class StoryItemSetContainerComponent: Component { self.trulyValidIds = trulyValidIds - var removeIds: [Int32] = [] + var removeIds: [StoryId] = [] for (id, visibleItem) in self.visibleItems { if !validIds.contains(id) { removeIds.append(id) @@ -1907,9 +1903,9 @@ public final class StoryItemSetContainerComponent: Component { func updateIsProgressPaused() { let progressMode = self.itemProgressMode() - var centralId: Int32? + var centralId: StoryId? if let component = self.component { - centralId = component.slice.item.storyItem.id + centralId = component.slice.item.id } for (id, visibleItem) in self.visibleItems { @@ -1931,27 +1927,27 @@ public final class StoryItemSetContainerComponent: Component { } var displayViewLists = false - if component.slice.peer.id == component.context.account.peerId { + if component.slice.effectivePeer.id == component.context.account.peerId { displayViewLists = true - } else if case let .channel(channel) = component.slice.peer, channel.flags.contains(.isCreator) || component.slice.additionalPeerData.canViewStats { + } else if case let .channel(channel) = component.slice.effectivePeer, channel.flags.contains(.isCreator) || component.slice.additionalPeerData.canViewStats { displayViewLists = true } if displayViewLists { self.viewListDisplayState = .half - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) self.dismissAllTooltips() return true } else { var canReply = false - if case .user = component.slice.peer { + if case .user = component.slice.effectivePeer { canReply = true - if component.slice.peer.id == component.context.account.peerId { + if component.slice.effectivePeer.id == component.context.account.peerId { canReply = false - } else if component.slice.peer.isService { + } else if component.slice.effectivePeer.isService { canReply = false } else if case .unsupported = component.slice.item.storyItem.media { canReply = false @@ -1974,12 +1970,12 @@ public final class StoryItemSetContainerComponent: Component { } var canReply = false - if case .user = component.slice.peer { + if case .user = component.slice.effectivePeer { canReply = true - if component.slice.peer.id == component.context.account.peerId { + if component.slice.effectivePeer.id == component.context.account.peerId { canReply = false - } else if component.slice.peer.isService { + } else if component.slice.effectivePeer.isService { canReply = false } else if case .unsupported = component.slice.item.storyItem.media { canReply = false @@ -1995,7 +1991,7 @@ public final class StoryItemSetContainerComponent: Component { if self.displayLikeReactions { self.displayLikeReactions = false - self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))) } inputPanelView.activateInput() @@ -2040,7 +2036,7 @@ public final class StoryItemSetContainerComponent: Component { captionItemView.layer.animateAlpha(from: 0.0, to: captionItemView.alpha, duration: 0.28) } - if let component = self.component, let sourceView = transitionIn.sourceView, let visibleItem = self.visibleItems[component.slice.item.storyItem.id] { + if let component = self.component, let sourceView = transitionIn.sourceView, let visibleItem = self.visibleItems[component.slice.item.id] { let contentContainerView = visibleItem.contentContainerView let unclippedContainerView = visibleItem.unclippedContainerView @@ -2116,7 +2112,7 @@ public final class StoryItemSetContainerComponent: Component { self.controlsNavigationClippingView.layer.animatePosition(from: sourceLocalFrame.center, to: self.controlsNavigationClippingView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) self.controlsNavigationClippingView.layer.animateBounds(from: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), to: self.controlsNavigationClippingView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - if let component = self.component, let visibleItemView = self.visibleItems[component.slice.item.storyItem.id]?.view.view { + if let component = self.component, let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view { let innerScale = innerSourceLocalFrame.width / visibleItemView.bounds.width let innerFromFrame = CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: CGSize(width: innerSourceLocalFrame.width, height: visibleItemView.bounds.height * innerScale)) @@ -2224,7 +2220,7 @@ public final class StoryItemSetContainerComponent: Component { }) } - if let component = self.component, let sourceView = transitionOut.destinationView, let visibleItem = self.visibleItems[component.slice.item.storyItem.id] { + if let component = self.component, let sourceView = transitionOut.destinationView, let visibleItem = self.visibleItems[component.slice.item.id] { if let footerPanelView = visibleItem.footerPanel?.view { footerPanelView.layer.animatePosition( from: CGPoint(), @@ -2315,7 +2311,7 @@ public final class StoryItemSetContainerComponent: Component { ), .immediate) } - let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) for transitionViewImpl in transitionViewsImpl { transitionViewImpl.alpha = 1.0 @@ -2438,7 +2434,7 @@ public final class StoryItemSetContainerComponent: Component { ), .immediate) } - let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) for transitionViewImpl in transitionViewsImpl { transitionViewImpl.alpha = 1.0 @@ -2461,7 +2457,7 @@ public final class StoryItemSetContainerComponent: Component { } } - if let component = self.component, let visibleItemView = self.visibleItems[component.slice.item.storyItem.id]?.view.view { + if let component = self.component, let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view { let innerScale = innerSourceLocalFrame.width / visibleItemView.bounds.width var adjustedInnerSourceLocalFrame = innerSourceLocalFrame @@ -2515,7 +2511,7 @@ public final class StoryItemSetContainerComponent: Component { if likeButtonView.alpha == 0.0 { return } - if component.slice.peer.isService { + if component.slice.effectivePeer.isService { return } else if case .unsupported = component.slice.item.storyItem.media { return @@ -2558,12 +2554,12 @@ public final class StoryItemSetContainerComponent: Component { let previousInput = inputPanelView.getSendMessageInput() switch previousInput { case let .text(value): - component.storyItemSharedState.replyDrafts[StoryId(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id)] = value + component.storyItemSharedState.replyDrafts[component.slice.item.id] = value } } } - func update(component: StoryItemSetContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryItemSetContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdatingComponent = true defer { self.isUpdatingComponent = false @@ -2600,7 +2596,7 @@ public final class StoryItemSetContainerComponent: Component { var isFirstItem = false var itemChanged = false var resetInputContents: MessageInputPanelComponent.SendMessageInput? - if self.component?.slice.item.storyItem.id != component.slice.item.storyItem.id { + if self.component?.slice.item.id != component.slice.item.id { isFirstItem = self.component == nil itemChanged = self.component != nil self.initializedOffset = false @@ -2608,10 +2604,10 @@ public final class StoryItemSetContainerComponent: Component { resetInputContents = .text(NSAttributedString()) self.saveDraft() - if let draft = component.storyItemSharedState.replyDrafts[StoryId(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id)] { + if let draft = component.storyItemSharedState.replyDrafts[component.slice.item.id] { resetInputContents = .text(draft) } - component.storyItemSharedState.replyDrafts.removeValue(forKey: StoryId(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id)) + component.storyItemSharedState.replyDrafts.removeValue(forKey: component.slice.item.id) if let tooltipScreen = self.sendMessageContext.tooltipScreen { if let tooltipScreen = tooltipScreen as? UndoOverlayController, let tag = tooltipScreen.tag as? String, tag == "no_auto_dismiss" { @@ -2628,7 +2624,7 @@ public final class StoryItemSetContainerComponent: Component { if let reactionContextNode = self.reactionContextNode { self.reactionContextNode = nil - let reactionTransition = Transition.immediate + let reactionTransition = ComponentTransition.immediate reactionTransition.setAlpha(view: reactionContextNode.view, alpha: 0.0, completion: { [weak reactionContextNode] _ in reactionContextNode?.view.removeFromSuperview() }) @@ -2640,7 +2636,7 @@ public final class StoryItemSetContainerComponent: Component { } var itemsTransition = transition var resetScrollingOffsetWithItemTransition = false - if let animateNextNavigationId = self.animateNextNavigationId, animateNextNavigationId == component.slice.item.storyItem.id { + if let animateNextNavigationId = self.animateNextNavigationId, animateNextNavigationId == component.slice.item.id { self.animateNextNavigationId = nil self.viewListPanState = nil self.isCompletingViewListPan = true @@ -2657,7 +2653,7 @@ public final class StoryItemSetContainerComponent: Component { resetScrollingOffsetWithItemTransition = true } - if let awaitingSwitchToId = self.awaitingSwitchToId, awaitingSwitchToId.to == component.slice.item.storyItem.id { + if let awaitingSwitchToId = self.awaitingSwitchToId, awaitingSwitchToId.to == component.slice.item.id { self.awaitingSwitchToId = nil self.viewListPanState = nil self.isCompletingViewListPan = true @@ -2761,7 +2757,7 @@ public final class StoryItemSetContainerComponent: Component { var isGroup = false var showMessageInputPanel = true var isGroupCommentRestricted = false - if case let .channel(channel) = component.slice.peer { + if case let .channel(channel) = component.slice.effectivePeer { switch channel.info { case .broadcast: isChannel = true @@ -2777,7 +2773,7 @@ public final class StoryItemSetContainerComponent: Component { isGroup = true } } else { - showMessageInputPanel = component.slice.peer.id != component.context.account.peerId + showMessageInputPanel = component.slice.effectivePeer.id != component.context.account.peerId } var isUnsupported = false @@ -2788,10 +2784,10 @@ public final class StoryItemSetContainerComponent: Component { self?.presentBoostToUnrestrict() }) } else if component.slice.additionalPeerData.isPremiumRequiredForMessaging { - disabledPlaceholder = .premiumRequired(title: component.strings.Story_MessagingRestrictedPlaceholder(component.slice.peer.compactDisplayTitle).string, subtitle: component.strings.Story_MessagingRestrictedPlaceholderAction, action: { [weak self] in + disabledPlaceholder = .premiumRequired(title: component.strings.Story_MessagingRestrictedPlaceholder(component.slice.effectivePeer.compactDisplayTitle).string, subtitle: component.strings.Story_MessagingRestrictedPlaceholderAction, action: { [weak self] in self?.presentPremiumRequiredForMessaging() }) - } else if component.slice.peer.isService { + } else if component.slice.effectivePeer.isService { disabledPlaceholder = .text(component.strings.Story_FooterReplyUnavailable) } else if case .unsupported = component.slice.item.storyItem.media { isUnsupported = true @@ -2860,10 +2856,10 @@ public final class StoryItemSetContainerComponent: Component { if showMessageInputPanel { var haveLikeOptions = false - if case .user = component.slice.peer { + if case .user = component.slice.effectivePeer { haveLikeOptions = true - if component.slice.peer.isService { + if component.slice.effectivePeer.isService { haveLikeOptions = false } } @@ -2930,7 +2926,7 @@ public final class StoryItemSetContainerComponent: Component { return } self.sendMessageContext.lockMediaRecording() - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) }, stopAndPreviewMediaRecording: { [weak self] in guard let self else { @@ -2945,7 +2941,7 @@ public final class StoryItemSetContainerComponent: Component { self.sendMessageContext.videoRecorderValue?.dismissVideo() self.sendMessageContext.discardMediaRecordingPreview(view: self) }, - attachmentAction: component.slice.peer.isService ? nil : { [weak self] in + attachmentAction: component.slice.effectivePeer.isService ? nil : { [weak self] in guard let self else { return } @@ -2975,7 +2971,7 @@ public final class StoryItemSetContainerComponent: Component { return MessageInputPanelComponent.MyReaction(reaction: value, file: centerAnimation, animationFileId: animationFileId) }, - likeAction: component.slice.peer.isService ? nil : { [weak self] in + likeAction: component.slice.effectivePeer.isService ? nil : { [weak self] in guard let self else { return } @@ -3018,12 +3014,12 @@ public final class StoryItemSetContainerComponent: Component { } let rect = view.convert(view.bounds, to: nil) let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let text = presentationData.strings.Conversation_VoiceMessagesRestricted(component.slice.peer.compactDisplayTitle).string + let text = presentationData.strings.Conversation_VoiceMessagesRestricted(component.slice.effectivePeer.compactDisplayTitle).string let controller = TooltipController(content: .text(text), baseFontSize: presentationData.listsFontSize.baseDisplaySize, isBlurred: true, padding: 2.0) controller.dismissed = { [weak self] _ in if let self { self.voiceMessagesRestrictedTooltipController = nil - self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut))) } } component.presentController(controller, TooltipControllerPresentationArguments(sourceViewAndRect: { [weak self] in @@ -3033,7 +3029,7 @@ public final class StoryItemSetContainerComponent: Component { return nil })) self.voiceMessagesRestrictedTooltipController = controller - self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut))) }, presentTextLengthLimitTooltip: nil, presentTextFormattingTooltip: nil, @@ -3173,52 +3169,52 @@ public final class StoryItemSetContainerComponent: Component { let startTime4 = CFAbsoluteTimeGetCurrent() - var validViewListIds: [Int32] = [] + var validViewListIds: [StoryId] = [] var displayViewLists = false - if component.slice.peer.id == component.context.account.peerId { + if component.slice.effectivePeer.id == component.context.account.peerId { displayViewLists = true - } else if case let .channel(channel) = component.slice.peer, channel.flags.contains(.isCreator) || component.slice.additionalPeerData.canViewStats { + } else if case let .channel(channel) = component.slice.effectivePeer, channel.flags.contains(.isCreator) || component.slice.additionalPeerData.canViewStats { displayViewLists = true } var viewListHeightMidFraction: CGFloat = 0.0 - if displayViewLists, let currentIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) { - var visibleViewListIds: [Int32] = [component.slice.item.storyItem.id] + if displayViewLists, let currentIndex = component.slice.allItems.firstIndex(where: { $0.id == component.slice.item.id }) { + var visibleViewListIds: [StoryId] = [component.slice.item.id] if self.viewListDisplayState != .hidden, let viewListPanState = self.viewListPanState { if currentIndex != 0 { if viewListPanState.fraction > 0.0 { - visibleViewListIds.append(component.slice.allItems[currentIndex - 1].storyItem.id) + visibleViewListIds.append(component.slice.allItems[currentIndex - 1].id) } } if currentIndex != component.slice.allItems.count - 1 { if viewListPanState.fraction < 0.0 { - visibleViewListIds.append(component.slice.allItems[currentIndex + 1].storyItem.id) + visibleViewListIds.append(component.slice.allItems[currentIndex + 1].id) } } } - var preloadViewListIds: [(Int32, EngineStoryItem.Views)] = [] + var preloadViewListIds: [(StoryId, EngineStoryItem.Views)] = [] if let views = component.slice.item.storyItem.views { - preloadViewListIds.append((component.slice.item.storyItem.id, views)) + preloadViewListIds.append((component.slice.item.id, views)) } if currentIndex != 0, let views = component.slice.allItems[currentIndex - 1].storyItem.views { - preloadViewListIds.append((component.slice.allItems[currentIndex - 1].storyItem.id, views)) + preloadViewListIds.append((component.slice.allItems[currentIndex - 1].id, views)) } if currentIndex != component.slice.allItems.count - 1, let views = component.slice.allItems[currentIndex + 1].storyItem.views { - preloadViewListIds.append((component.slice.allItems[currentIndex + 1].storyItem.id, views)) + preloadViewListIds.append((component.slice.allItems[currentIndex + 1].id, views)) } for (id, views) in preloadViewListIds { - if component.sharedViewListsContext.viewLists[StoryId(peerId: component.slice.peer.id, id: id)] == nil { + if component.sharedViewListsContext.viewLists[id] == nil { let defaultSortMode: EngineStoryViewListContext.SortMode - if component.slice.peer.id.isGroupOrChannel { + if component.slice.effectivePeer.id.isGroupOrChannel { defaultSortMode = .repostsFirst } else { defaultSortMode = .reactionsFirst } - let viewList = component.context.engine.messages.storyViewList(peerId: component.slice.peer.id, id: id, views: views, listMode: .everyone, sortMode: defaultSortMode) - component.sharedViewListsContext.viewLists[StoryId(peerId: component.slice.peer.id, id: id)] = viewList + let viewList = component.context.engine.messages.storyViewList(peerId: component.slice.effectivePeer.id, id: id.id, views: views, listMode: .everyone, sortMode: defaultSortMode) + component.sharedViewListsContext.viewLists[id] = viewList } } @@ -3236,7 +3232,7 @@ public final class StoryItemSetContainerComponent: Component { } var fixedAnimationOffset: CGFloat = 0.0 - var applyFixedAnimationOffsetIds: [Int32] = [] + var applyFixedAnimationOffsetIds: [StoryId] = [] let normalCollapsedContentAreaHeight: CGFloat = availableSize.height - minimizedHeight @@ -3281,7 +3277,7 @@ public final class StoryItemSetContainerComponent: Component { maximizedBottomContentHeight = defaultHeight for id in visibleViewListIds { - guard let itemIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == id }) else { + guard let itemIndex = component.slice.allItems.firstIndex(where: { $0.id == id }) else { continue } let item = component.slice.allItems[itemIndex] @@ -3304,7 +3300,7 @@ public final class StoryItemSetContainerComponent: Component { safeInsets.bottom = max(safeInsets.bottom, component.inputHeight) var hasPremium = false - if case let .user(user) = component.slice.peer { + if case let .user(user) = component.slice.effectivePeer { hasPremium = user.isPremium } @@ -3320,7 +3316,7 @@ public final class StoryItemSetContainerComponent: Component { theme: component.theme, strings: component.strings, sharedListsContext: component.sharedViewListsContext, - peerId: component.slice.peer.id, + peerId: component.slice.effectivePeer.id, safeInsets: safeInsets, storyItem: item.storyItem, hasPremium: hasPremium, @@ -3334,7 +3330,7 @@ public final class StoryItemSetContainerComponent: Component { } self.viewListDisplayState = .hidden self.isSearchActive = false - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) }, expandViewStats: { }, @@ -3438,7 +3434,7 @@ public final class StoryItemSetContainerComponent: Component { action: { _ in return false } ), nil) }))) - } else { + } else if isContact { itemList.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextHideStoriesFrom(peer.compactDisplayTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Stories"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in @@ -3596,7 +3592,7 @@ public final class StoryItemSetContainerComponent: Component { self.viewListDisplayState = .half } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.5, curve: .spring))) } }, controller: { [weak self] in @@ -3626,7 +3622,7 @@ public final class StoryItemSetContainerComponent: Component { viewListView.animateIn(transition: transition) } } - /*if id == component.slice.item.storyItem.id { + /*if id == component.slice.item.id { viewListInset = minimizedHeight * viewList.externalState.minimizationFraction + defaultHeight * (1.0 - viewList.externalState.minimizationFraction) inputPanelBottomInset = viewListInset minimizedBottomContentHeight = minimizedHeight @@ -3637,7 +3633,7 @@ public final class StoryItemSetContainerComponent: Component { if fixedAnimationOffset == 0.0 { for (id, viewList) in self.viewLists { - if let viewListView = viewList.view.view, !visibleViewListIds.contains(id), let itemIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == id }) { + if let viewListView = viewList.view.view, !visibleViewListIds.contains(id), let itemIndex = component.slice.allItems.firstIndex(where: { $0.id == id }) { let viewListSize = viewListView.bounds.size var viewListFrame = CGRect(origin: CGPoint(x: viewListBaseOffsetX, y: availableSize.height - viewListSize.height), size: viewListSize) let indexDistance = CGFloat(max(-1, min(1, itemIndex - currentIndex))) @@ -3658,7 +3654,7 @@ public final class StoryItemSetContainerComponent: Component { } else { self.viewListMetrics = nil } - var removeViewListIds: [Int32] = [] + var removeViewListIds: [StoryId] = [] for (id, viewList) in self.viewLists { if !validViewListIds.contains(id) { removeViewListIds.append(id) @@ -3903,14 +3899,14 @@ public final class StoryItemSetContainerComponent: Component { } let storyPrivacyIcon: StoryPrivacyIconComponent.Privacy? - if case .user = component.slice.peer { + if case .user = component.slice.effectivePeer { if component.slice.item.storyItem.isCloseFriends { storyPrivacyIcon = .closeFriends } else if component.slice.item.storyItem.isContacts { storyPrivacyIcon = .contacts } else if component.slice.item.storyItem.isSelectedContacts { storyPrivacyIcon = .selectedContacts - } else if component.slice.peer.id == component.context.account.peerId { + } else if component.slice.effectivePeer.id == component.context.account.peerId { storyPrivacyIcon = .everyone } else { storyPrivacyIcon = nil @@ -3921,7 +3917,7 @@ public final class StoryItemSetContainerComponent: Component { if let storyPrivacyIcon { let privacyIcon: ComponentView - var privacyIconTransition: Transition = itemChanged ? .immediate : .easeInOut(duration: 0.2) + var privacyIconTransition: ComponentTransition = itemChanged ? .immediate : .easeInOut(duration: 0.2) if let current = self.privacyIcon { privacyIcon = current } else { @@ -3935,7 +3931,7 @@ public final class StoryItemSetContainerComponent: Component { content: AnyComponent( StoryPrivacyIconComponent( privacy: storyPrivacyIcon, - isEditable: component.slice.peer.id == component.context.account.peerId + isEditable: component.slice.effectivePeer.id == component.context.account.peerId ) ), effectAlignment: .center, @@ -3943,7 +3939,7 @@ public final class StoryItemSetContainerComponent: Component { guard let self, let component = self.component else { return } - if component.slice.peer.id == component.context.account.peerId { + if component.slice.effectivePeer.id == component.context.account.peerId { self.openItemPrivacySettings() return } @@ -3953,11 +3949,11 @@ public final class StoryItemSetContainerComponent: Component { let tooltipText: String switch storyPrivacyIcon { case .closeFriends: - tooltipText = component.strings.Story_TooltipPrivacyCloseFriends2(component.slice.peer.compactDisplayTitle).string + tooltipText = component.strings.Story_TooltipPrivacyCloseFriends2(component.slice.effectivePeer.compactDisplayTitle).string case .contacts: - tooltipText = component.strings.Story_TooltipPrivacyContacts(component.slice.peer.compactDisplayTitle).string + tooltipText = component.strings.Story_TooltipPrivacyContacts(component.slice.effectivePeer.compactDisplayTitle).string case .selectedContacts: - tooltipText = component.strings.Story_TooltipPrivacySelectedContacts(component.slice.peer.compactDisplayTitle).string + tooltipText = component.strings.Story_TooltipPrivacySelectedContacts(component.slice.effectivePeer.compactDisplayTitle).string case .everyone: tooltipText = "" } @@ -4011,7 +4007,7 @@ public final class StoryItemSetContainerComponent: Component { var currentLeftInfoItem: InfoItem? if focusedItem != nil { - let leftInfoComponent = AnyComponent(StoryAvatarInfoComponent(context: component.context, peer: component.slice.peer)) + let leftInfoComponent = AnyComponent(StoryAvatarInfoComponent(context: component.context, peer: component.slice.effectivePeer)) if let leftInfoItem = self.leftInfoItem, leftInfoItem.component == leftInfoComponent { currentLeftInfoItem = leftInfoItem } else { @@ -4042,7 +4038,7 @@ public final class StoryItemSetContainerComponent: Component { let centerInfoComponent = AnyComponent(StoryAuthorInfoComponent( context: component.context, strings: component.strings, - peer: component.slice.peer, + peer: component.slice.effectivePeer, forwardInfo: component.slice.item.storyItem.forwardInfo, author: component.slice.item.storyItem.author, timestamp: component.slice.item.storyItem.timestamp, @@ -4085,10 +4081,10 @@ public final class StoryItemSetContainerComponent: Component { self.navigateToPeer(peer: author, chat: false) } } else { - if component.slice.peer.id == component.context.account.peerId { + if component.slice.effectivePeer.id == component.context.account.peerId { self.navigateToMyStories() } else { - self.navigateToPeer(peer: component.slice.peer, chat: false) + self.navigateToPeer(peer: component.slice.effectivePeer, chat: false) } } })), @@ -4120,10 +4116,10 @@ public final class StoryItemSetContainerComponent: Component { guard let self, let component = self.component else { return } - if component.slice.peer.id == component.context.account.peerId { + if component.slice.effectivePeer.id == component.context.account.peerId { self.navigateToMyStories() } else { - self.navigateToPeer(peer: component.slice.peer, chat: false) + self.navigateToPeer(peer: component.slice.effectivePeer, chat: false) } })), environment: {}, @@ -4157,7 +4153,7 @@ public final class StoryItemSetContainerComponent: Component { if let inputPanelSize { let inputPanelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - inputPanelSize.width) / 2.0), y: availableSize.height - inputPanelBottomInset - inputPanelSize.height), size: inputPanelSize) inputPanelFrameValue = inputPanelFrame - var inputPanelAlpha: CGFloat = (component.slice.peer.id == component.context.account.peerId || component.hideUI || self.isEditingStory || component.slice.item.storyItem.isPending) ? 0.0 : 1.0 + var inputPanelAlpha: CGFloat = (component.slice.effectivePeer.id == component.context.account.peerId || component.hideUI || self.isEditingStory || component.slice.item.storyItem.isPending) ? 0.0 : 1.0 if case .regular = component.metrics.widthClass { inputPanelAlpha *= component.visibilityFraction } @@ -4169,7 +4165,7 @@ public final class StoryItemSetContainerComponent: Component { } var inputPanelOffset: CGFloat = 0.0 - if component.slice.peer.id != component.context.account.peerId && !self.inputPanelExternalState.isEditing { + if component.slice.effectivePeer.id != component.context.account.peerId && !self.inputPanelExternalState.isEditing { let bandingOffset = scrollingRubberBandingOffset(offset: verticalPanFraction * availableSize.height, bandingStart: 0.0, range: 10.0) inputPanelOffset = -max(0.0, min(10.0, bandingOffset)) } @@ -4182,7 +4178,7 @@ public final class StoryItemSetContainerComponent: Component { } } - if let captionItem = self.captionItem, captionItem.itemId != component.slice.item.storyItem.id { + if let captionItem = self.captionItem, captionItem.itemId != component.slice.item.id { self.captionItem = nil if let captionItemView = captionItem.view.view { captionItemView.removeFromSuperview() @@ -4198,13 +4194,13 @@ public final class StoryItemSetContainerComponent: Component { if !transition.animation.isImmediate { captionItemTransition = .immediate } - captionItem = CaptionItem(itemId: component.slice.item.storyItem.id) + captionItem = CaptionItem(itemId: component.slice.item.id) self.captionItem = captionItem } var enableEntities = true - if case .user = component.slice.peer { - if !component.slice.peer.isService && !component.slice.peer.isPremium { + if case .user = component.slice.effectivePeer { + if !component.slice.effectivePeer.isService && !component.slice.effectivePeer.isPremium { enableEntities = false } } @@ -4220,7 +4216,7 @@ public final class StoryItemSetContainerComponent: Component { strings: component.strings, theme: component.theme, text: component.slice.item.storyItem.text, - author: component.slice.peer, + author: component.slice.effectivePeer, forwardInfo: component.slice.item.storyItem.forwardInfo, forwardInfoStory: forwardInfoStory, entities: enableEntities ? component.slice.item.storyItem.entities : [], @@ -4231,7 +4227,7 @@ public final class StoryItemSetContainerComponent: Component { } switch action { case let .url(url, concealed): - let _ = openUserGeneratedUrl(context: component.context, peerId: component.slice.peer.id, url: url, concealed: concealed, skipUrlAuth: false, skipConcealedAlert: false, forceDark: true, present: { [weak self] c in + let _ = openUserGeneratedUrl(context: component.context, peerId: component.slice.effectivePeer.id, url: url, concealed: concealed, skipUrlAuth: false, skipConcealedAlert: false, forceDark: true, present: { [weak self] c in guard let self, let component = self.component, let controller = component.controller() else { return } @@ -4265,7 +4261,7 @@ public final class StoryItemSetContainerComponent: Component { return } self.sendMessageContext.presentTextEntityActions(view: self, action: action, openUrl: { [weak self] url, concealed in - let _ = openUserGeneratedUrl(context: component.context, peerId: component.slice.peer.id, url: url, concealed: concealed, skipUrlAuth: false, skipConcealedAlert: false, present: { [weak self] c in + let _ = openUserGeneratedUrl(context: component.context, peerId: component.slice.effectivePeer.id, url: url, concealed: concealed, skipUrlAuth: false, skipConcealedAlert: false, present: { [weak self] c in guard let self, let component = self.component, let controller = component.controller() else { return } @@ -4325,7 +4321,7 @@ public final class StoryItemSetContainerComponent: Component { } if let story { let context = component.context - let peerId = component.slice.peer.id + let peerId = component.slice.effectivePeer.id let currentResult: ResolvedUrl = .story(peerId: peerId, id: component.slice.item.storyItem.id) self.sendMessageContext.openResolved(view: self, result: .story(peerId: peer.id, id: story.id), completion: { [weak self] in @@ -4484,19 +4480,19 @@ public final class StoryItemSetContainerComponent: Component { guard let self else { return } - self.state?.updated(transition: Transition(transition)) + self.state?.updated(transition: ComponentTransition(transition)) }, requestLayout: { [weak self] transition in guard let self else { return } - self.state?.updated(transition: Transition(transition)) + self.state?.updated(transition: ComponentTransition(transition)) }, requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in guard let self else { return } - self.state?.updated(transition: Transition(transition)) + self.state?.updated(transition: ComponentTransition(transition)) } ) reactionContextNode.displayTail = self.displayLikeReactions @@ -4535,18 +4531,18 @@ public final class StoryItemSetContainerComponent: Component { if self.displayLikeReactions { if component.slice.item.storyItem.myReaction == updateReaction.reaction { - let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: nil).startStandalone() + let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.effectivePeer.id, id: component.slice.item.storyItem.id, reaction: nil).startStandalone() self.displayLikeReactions = false - self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))) } else { if hasFirstResponder(self) { self.sendMessageContext.currentInputMode = .text self.endEditing(true) } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))) self.waitingForReactionAnimateOutToLike = updateReaction.reaction - let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: updateReaction.reaction).startStandalone() + let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.effectivePeer.id, id: component.slice.item.id.id, reaction: updateReaction.reaction).startStandalone() } } else { let _ = (component.context.engine.stickers.availableReactions() @@ -4597,7 +4593,7 @@ public final class StoryItemSetContainerComponent: Component { self.sendMessageContext.currentInputMode = .text self.endEditing(true) } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))) var text = "" var messageAttributes: [MessageAttribute] = [] @@ -4630,7 +4626,7 @@ public final class StoryItemSetContainerComponent: Component { mediaReference: nil, threadId: nil, replyToMessageId: nil, - replyToStoryId: StoryId(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id), + replyToStoryId: component.slice.item.id, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [] @@ -4639,7 +4635,7 @@ public final class StoryItemSetContainerComponent: Component { let context = component.context let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) let presentController = component.presentController - let peer = component.slice.peer + let peer = component.slice.effectivePeer let _ = (enqueueMessages(account: context.account, peerId: peer.id, messages: [message]) |> deliverOnMainQueue).startStandalone(next: { [weak self] messageIds in @@ -4781,7 +4777,7 @@ public final class StoryItemSetContainerComponent: Component { return } self.displayLikeReactions = false - self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))) } } } else { @@ -4797,7 +4793,7 @@ public final class StoryItemSetContainerComponent: Component { reactionContextNode.animateOut(to: reactionsAnchorRect, animatingOutToReaction: true) } } else { - let reactionTransition = Transition.easeInOut(duration: 0.25) + let reactionTransition = ComponentTransition.easeInOut(duration: 0.25) reactionTransition.setAlpha(view: reactionContextNode.view, alpha: 0.0, completion: { [weak reactionContextNode] _ in reactionContextNode?.view.removeFromSuperview() }) @@ -4836,13 +4832,13 @@ public final class StoryItemSetContainerComponent: Component { transition.setFrame(view: self.contentDimView, frame: CGRect(origin: CGPoint(), size: contentFrame.size)) if transition.animation.isImmediate && forceDimAnimation && self.topContentGradientView.alpha != topGradientAlpha { - Transition(animation: .curve(duration: 0.25, curve: .easeInOut)).setAlpha(view: self.topContentGradientView, alpha: topGradientAlpha) + ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)).setAlpha(view: self.topContentGradientView, alpha: topGradientAlpha) } else { transition.setAlpha(view: self.topContentGradientView, alpha: topGradientAlpha) } if transition.animation.isImmediate && forceDimAnimation && self.contentDimView.alpha != dimAlpha { - Transition(animation: .curve(duration: 0.25, curve: .easeInOut)).setAlpha(view: self.contentDimView, alpha: dimAlpha) + ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)).setAlpha(view: self.contentDimView, alpha: dimAlpha) } else { transition.setAlpha(view: self.contentDimView, alpha: dimAlpha) } @@ -4853,7 +4849,7 @@ public final class StoryItemSetContainerComponent: Component { self.scroller.contentSize = CGSize(width: itemLayout.fullItemScrollDistance * CGFloat(max(0, component.slice.allItems.count - 1)) + availableSize.width, height: availableSize.height) self.scroller.isScrollEnabled = itemLayout.contentScaleFraction >= 1.0 - 0.0001 - if let centralIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) { + if let centralIndex = component.slice.allItems.firstIndex(where: { $0.id == component.slice.item.id }) { let centralX = itemLayout.fullItemScrollDistance * CGFloat(centralIndex) if itemLayout.contentScaleFraction <= 0.0001 { if abs(self.scroller.contentOffset.x - centralX) > CGFloat.ulpOfOne { @@ -4878,7 +4874,7 @@ public final class StoryItemSetContainerComponent: Component { let navigationStripSideInset: CGFloat = 8.0 let navigationStripTopInset: CGFloat = 8.0 - if let focusedItem, let visibleItem = self.visibleItems[focusedItem.storyItem.id], let index = focusedItem.position { + if let focusedItem, let visibleItem = self.visibleItems[focusedItem.id], let index = focusedItem.position { var index = max(0, min(index, component.slice.totalCount - 1)) var count = component.slice.totalCount if let dayCounters = focusedItem.dayCounters { @@ -4938,7 +4934,7 @@ public final class StoryItemSetContainerComponent: Component { } component.externalState.derivedMediaSize = contentFrame.size - if component.slice.peer.id == component.context.account.peerId { + if component.slice.effectivePeer.id == component.context.account.peerId { component.externalState.derivedBottomInset = availableSize.height - itemsContainerFrame.maxY } else if let inputPanelFrameValue { component.externalState.derivedBottomInset = availableSize.height - min(inputPanelFrameValue.minY, contentFrame.maxY) @@ -5286,12 +5282,14 @@ public final class StoryItemSetContainerComponent: Component { return } - guard let viewList = self.viewLists[component.slice.item.storyItem.id], let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View, let viewListContext = viewListView.currentViewList else { + let storyId = StoryId(peerId: peer.id, id: id) + + guard let viewList = self.viewLists[component.slice.item.id], let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View, let viewListContext = viewListView.currentViewList else { return } let context = component.context - let storyContent = RepostStoriesContentContextImpl(context: context, originalPeerId: component.slice.peer.id, originalStory: component.slice.item.storyItem, focusedStoryId: StoryId(peerId: peer.id, id: id), viewListContext: viewListContext, readGlobally: false) + let storyContent = RepostStoriesContentContextImpl(context: context, originalPeerId: component.slice.effectivePeer.id, originalStory: component.slice.item.storyItem, focusedStoryId: storyId, viewListContext: viewListContext, readGlobally: false) let _ = (storyContent.state |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak controller, weak viewListView, weak sourceView] _ in @@ -5368,13 +5366,13 @@ public final class StoryItemSetContainerComponent: Component { self.state?.updated(transition: .easeInOut(duration: 0.2)) var videoPlaybackPosition: Double? - if let visibleItem = self.visibleItems[component.slice.item.storyItem.id], let view = visibleItem.view.view as? StoryItemContentComponent.View { + if let visibleItem = self.visibleItems[component.slice.item.id], let view = visibleItem.view.view as? StoryItemContentComponent.View { videoPlaybackPosition = view.videoPlaybackPosition } guard let controller = MediaEditorScreen.makeEditStoryController( context: component.context, - peer: component.slice.peer, + peer: component.slice.effectivePeer, storyItem: component.slice.item.storyItem, videoPlaybackPosition: videoPlaybackPosition, repost: repost, @@ -5497,7 +5495,7 @@ public final class StoryItemSetContainerComponent: Component { HapticFeedback().impact() let _ = combineLatest(queue: Queue.mainQueue(), - component.context.engine.peers.getChannelBoostStatus(peerId: component.slice.peer.id), + component.context.engine.peers.getChannelBoostStatus(peerId: component.slice.effectivePeer.id), component.context.engine.peers.getMyBoostStatus() ).startStandalone(next: { [weak self] boostStatus, myBoostStatus in guard let self, let component = self.component, let boostStatus, let myBoostStatus else { @@ -5505,7 +5503,7 @@ public final class StoryItemSetContainerComponent: Component { } let boostController = PremiumBoostLevelsScreen( context: component.context, - peerId: component.slice.peer.id, + peerId: component.slice.effectivePeer.id, mode: .user(mode: .unrestrict(Int(boostsToUnrestrict))), status: boostStatus, myBoostStatus: myBoostStatus, @@ -5586,7 +5584,7 @@ public final class StoryItemSetContainerComponent: Component { } private func requestSave() { - guard let component = self.component, let peerReference = PeerReference(component.slice.peer._asPeer()) else { + guard let component = self.component, let peerReference = PeerReference(component.slice.effectivePeer._asPeer()) else { return } @@ -5625,9 +5623,9 @@ public final class StoryItemSetContainerComponent: Component { guard let component = self.component else { return } - if component.slice.peer.id == component.context.account.peerId { + if component.slice.effectivePeer.id == component.context.account.peerId { self.performMyMoreAction(sourceView: sourceView, gesture: gesture) - } else if case let .channel(channel) = component.slice.peer { + } else if case let .channel(channel) = component.slice.effectivePeer { var canPerformStoryActions = false if channel.hasPermission(.editStories) { @@ -5665,7 +5663,7 @@ public final class StoryItemSetContainerComponent: Component { var likeButtonView: UIView? var addTracingOffset: ((UIView) -> Void)? - if let visibleItem = self.visibleItems[component.slice.item.storyItem.id], let footerPanelView = visibleItem.footerPanel?.view as? StoryFooterPanelComponent.View { + if let visibleItem = self.visibleItems[component.slice.item.id], let footerPanelView = visibleItem.footerPanel?.view as? StoryFooterPanelComponent.View { likeButtonView = footerPanelView.likeButtonView addTracingOffset = { [weak footerPanelView] view in footerPanelView?.setLikeButtonTracingOffset(view: view) @@ -5681,7 +5679,7 @@ public final class StoryItemSetContainerComponent: Component { return } - let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: component.slice.item.storyItem.myReaction == nil ? .builtin("❤") : nil).startStandalone() + let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.effectivePeer.id, id: component.slice.item.id.id, reaction: component.slice.item.storyItem.myReaction == nil ? .builtin("❤") : nil).startStandalone() if component.slice.item.storyItem.myReaction != nil { return @@ -5758,7 +5756,7 @@ public final class StoryItemSetContainerComponent: Component { self.displayLikeReactions = true self.tempReactionsGesture = gesture - self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))) self.updateIsProgressPaused() self.tempReactionsGesture = nil } @@ -5808,14 +5806,14 @@ public final class StoryItemSetContainerComponent: Component { } } - if !emojiFileIds.isEmpty || hasLinkedStickers, let peerReference = PeerReference(component.slice.peer._asPeer()) { + if !emojiFileIds.isEmpty || hasLinkedStickers, let peerReference = PeerReference(component.slice.effectivePeer._asPeer()) { let context = component.context tip = .animatedEmoji(text: nil, arguments: nil, file: nil, action: nil) let packsPromise = Promise<[StickerPackReference]>() if hasLinkedStickers { - packsPromise.set(context.engine.stickers.stickerPacksAttachedToMedia(media: .story(peer: peerReference, id: component.slice.item.storyItem.id, media: media))) + packsPromise.set(context.engine.stickers.stickerPacksAttachedToMedia(media: .story(peer: peerReference, id: component.slice.item.id.id, media: media))) } else { packsPromise.set(.single([])) } @@ -5939,7 +5937,7 @@ public final class StoryItemSetContainerComponent: Component { } let rate = normalizeValue(newValue) - if let visibleItem = self.visibleItems[component.slice.item.storyItem.id], let view = visibleItem.view.view as? StoryItemContentComponent.View { + if let visibleItem = self.visibleItems[component.slice.item.id], let view = visibleItem.view.view as? StoryItemContentComponent.View { view.setBaseRate(rate) } @@ -5969,7 +5967,7 @@ public final class StoryItemSetContainerComponent: Component { return } - if let visibleItem = self.visibleItems[component.slice.item.storyItem.id], let view = visibleItem.view.view as? StoryItemContentComponent.View { + if let visibleItem = self.visibleItems[component.slice.item.id], let view = visibleItem.view.view as? StoryItemContentComponent.View { view.setBaseRate(rate) } component.storyItemSharedState.baseRate = rate @@ -6081,7 +6079,7 @@ public final class StoryItemSetContainerComponent: Component { return } - let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.slice.peer.id, ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).startStandalone() + let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.slice.effectivePeer.id, ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).startStandalone() let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) if component.slice.item.storyItem.isPinned { @@ -6117,7 +6115,7 @@ public final class StoryItemSetContainerComponent: Component { self.requestSave() }))) - if case let .user(accountUser) = component.slice.peer { + if case let .user(accountUser) = component.slice.effectivePeer { items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextStealthMode, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in @@ -6134,7 +6132,7 @@ public final class StoryItemSetContainerComponent: Component { }))) } - if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) && (component.slice.item.storyItem.expirationTimestamp > Int32(Date().timeIntervalSince1970) || component.slice.item.storyItem.isPinned) { + if component.slice.item.storyItem.isPublic && (component.slice.effectivePeer.addressName != nil || !component.slice.effectivePeer._asPeer().usernames.isEmpty) && (component.slice.item.storyItem.expirationTimestamp > Int32(Date().timeIntervalSince1970) || component.slice.item.storyItem.isPinned) { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_CopyLink, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in @@ -6144,7 +6142,7 @@ public final class StoryItemSetContainerComponent: Component { return } - let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id) + let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.effectivePeer.id, id: component.slice.item.storyItem.id) |> deliverOnMainQueue).startStandalone(next: { [weak self] link in guard let self, let component = self.component else { return @@ -6197,7 +6195,7 @@ public final class StoryItemSetContainerComponent: Component { guard let component = self.component, let controller = component.controller() else { return } - guard case let .channel(channel) = component.slice.peer else { + guard case let .channel(channel) = component.slice.effectivePeer else { return } @@ -6270,10 +6268,10 @@ public final class StoryItemSetContainerComponent: Component { return } - let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.slice.peer.id, ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).startStandalone() + let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.slice.effectivePeer.id, ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).startStandalone() var isGroup = false - if case let .channel(channel) = component.slice.peer, case .group = channel.info { + if case let .channel(channel) = component.slice.effectivePeer, case .group = channel.info { isGroup = true } @@ -6313,7 +6311,7 @@ public final class StoryItemSetContainerComponent: Component { let statsController = component.context.sharedContext.makeStoryStatsController( context: component.context, updatedPresentationData: (presentationData, .single(presentationData)), - peerId: component.slice.peer.id, + peerId: component.slice.effectivePeer.id, storyId: component.slice.item.storyItem.id, storyItem: component.slice.item.storyItem, fromStory: true @@ -6334,7 +6332,7 @@ public final class StoryItemSetContainerComponent: Component { self.requestSave() }))) - if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) && (component.slice.item.storyItem.expirationTimestamp > Int32(Date().timeIntervalSince1970) || component.slice.item.storyItem.isPinned) { + if component.slice.item.storyItem.isPublic && (component.slice.effectivePeer.addressName != nil || !component.slice.effectivePeer._asPeer().usernames.isEmpty) && (component.slice.item.storyItem.expirationTimestamp > Int32(Date().timeIntervalSince1970) || component.slice.item.storyItem.isPinned) { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_CopyLink, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in @@ -6344,7 +6342,7 @@ public final class StoryItemSetContainerComponent: Component { return } - let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id) + let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.effectivePeer.id, id: component.slice.item.storyItem.id) |> deliverOnMainQueue).startStandalone(next: { [weak self] link in guard let self, let component = self.component else { return @@ -6376,7 +6374,7 @@ public final class StoryItemSetContainerComponent: Component { } var isHidden = false - if case let .channel(channel) = component.slice.peer, let storiesHidden = channel.storiesHidden { + if case let .channel(channel) = component.slice.effectivePeer, let storiesHidden = channel.storiesHidden { isHidden = storiesHidden } items.append(.action(ContextMenuActionItem(text: isHidden ? component.strings.StoryFeed_ContextUnarchive : component.strings.StoryFeed_ContextArchive, icon: { theme in @@ -6388,20 +6386,20 @@ public final class StoryItemSetContainerComponent: Component { return } - let _ = component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.peer.id, isHidden: !isHidden) + let _ = component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.effectivePeer.id, isHidden: !isHidden) - let text = !isHidden ? component.strings.StoryFeed_TooltipArchive(component.slice.peer.compactDisplayTitle).string : component.strings.StoryFeed_TooltipUnarchive(component.slice.peer.compactDisplayTitle).string + let text = !isHidden ? component.strings.StoryFeed_TooltipArchive(component.slice.effectivePeer.compactDisplayTitle).string : component.strings.StoryFeed_TooltipUnarchive(component.slice.effectivePeer.compactDisplayTitle).string let tooltipScreen = TooltipScreen( context: component.context, account: component.context.account, sharedContext: component.context.sharedContext, text: .markdown(text: text), style: .customBlur(UIColor(rgb: 0x1c1c1c), 0.0), - icon: .peer(peer: component.slice.peer, isStory: true), + icon: .peer(peer: component.slice.effectivePeer, isStory: true), action: TooltipScreen.Action( title: component.strings.Undo_Undo, action: { - component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.peer.id, isHidden: isHidden) + component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.effectivePeer.id, isHidden: isHidden) } ), location: .bottom, @@ -6503,10 +6501,10 @@ public final class StoryItemSetContainerComponent: Component { let _ = (combineLatest( queue: Queue.mainQueue(), component.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: component.slice.peer.id), + TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: component.slice.effectivePeer.id), TelegramEngine.EngineData.Item.NotificationSettings.Global(), TelegramEngine.EngineData.Item.Contacts.Top(), - TelegramEngine.EngineData.Item.Peer.IsContact(id: component.slice.peer.id), + TelegramEngine.EngineData.Item.Peer.IsContact(id: component.slice.effectivePeer.id), TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId) ), translationSettings, @@ -6558,9 +6556,9 @@ public final class StoryItemSetContainerComponent: Component { items.append(.separator) } - let isMuted = resolvedAreStoriesMuted(globalSettings: globalSettings._asGlobalNotificationSettings(), peer: component.slice.peer._asPeer(), peerSettings: settings._asNotificationSettings(), topSearchPeers: topSearchPeers) + let isMuted = resolvedAreStoriesMuted(globalSettings: globalSettings._asGlobalNotificationSettings(), peer: component.slice.effectivePeer._asPeer(), peerSettings: settings._asNotificationSettings(), topSearchPeers: topSearchPeers) - if !component.slice.peer.isService && isContact { + if !component.slice.effectivePeer.isService && isContact { items.append(.action(ContextMenuActionItem(text: isMuted ? component.strings.StoryFeed_ContextNotifyOn : component.strings.StoryFeed_ContextNotifyOff, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: component.slice.additionalPeerData.isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in @@ -6570,7 +6568,7 @@ public final class StoryItemSetContainerComponent: Component { return } - let _ = component.context.engine.peers.togglePeerStoriesMuted(peerId: component.slice.peer.id).startStandalone() + let _ = component.context.engine.peers.togglePeerStoriesMuted(peerId: component.slice.effectivePeer.id).startStandalone() let iconColor = UIColor.white let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) @@ -6583,7 +6581,7 @@ public final class StoryItemSetContainerComponent: Component { "Bottom.Group 1.Fill 1": iconColor, "EXAMPLE.Group 1.Fill 1": iconColor, "Line.Group 1.Stroke 1": iconColor - ], title: nil, text: component.strings.StoryFeed_TooltipNotifyOn(component.slice.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string, customUndoText: nil, timeout: nil), + ], title: nil, text: component.strings.StoryFeed_TooltipNotifyOn(component.slice.effectivePeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, blurred: true, @@ -6598,7 +6596,7 @@ public final class StoryItemSetContainerComponent: Component { "Bottom.Group 1.Fill 1": iconColor, "EXAMPLE.Group 1.Fill 1": iconColor, "Line.Group 1.Stroke 1": iconColor - ], title: nil, text: component.strings.StoryFeed_TooltipNotifyOff(component.slice.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string, customUndoText: nil, timeout: nil), + ], title: nil, text: component.strings.StoryFeed_TooltipNotifyOff(component.slice.effectivePeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, blurred: true, @@ -6608,7 +6606,7 @@ public final class StoryItemSetContainerComponent: Component { }))) } - if !component.slice.peer.isService && component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) { + if !component.slice.effectivePeer.isService && component.slice.item.storyItem.isPublic && (component.slice.effectivePeer.addressName != nil || !component.slice.effectivePeer._asPeer().usernames.isEmpty) { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_CopyLink, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in @@ -6618,7 +6616,7 @@ public final class StoryItemSetContainerComponent: Component { return } - let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id) + let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.effectivePeer.id, id: component.slice.item.storyItem.id) |> deliverOnMainQueue).startStandalone(next: { [weak self] link in guard let self, let component = self.component else { return @@ -6688,9 +6686,9 @@ public final class StoryItemSetContainerComponent: Component { } var isHidden = false - if case let .user(user) = component.slice.peer, let storiesHidden = user.storiesHidden { + if case let .user(user) = component.slice.effectivePeer, let storiesHidden = user.storiesHidden { isHidden = storiesHidden - } else if case let .channel(channel) = component.slice.peer, let storiesHidden = channel.storiesHidden { + } else if case let .channel(channel) = component.slice.effectivePeer, let storiesHidden = channel.storiesHidden { isHidden = storiesHidden } @@ -6698,9 +6696,9 @@ public final class StoryItemSetContainerComponent: Component { if isHidden { canArchive = true } else { - if case .user = component.slice.peer, !component.slice.peer.isService { + if case .user = component.slice.effectivePeer, !component.slice.effectivePeer.isService { canArchive = true - } else if case .channel = component.slice.peer { + } else if case .channel = component.slice.effectivePeer { canArchive = true } } @@ -6715,20 +6713,20 @@ public final class StoryItemSetContainerComponent: Component { return } - let _ = component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.peer.id, isHidden: !isHidden) + let _ = component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.effectivePeer.id, isHidden: !isHidden) - let text = !isHidden ? component.strings.StoryFeed_TooltipArchive(component.slice.peer.compactDisplayTitle).string : component.strings.StoryFeed_TooltipUnarchive(component.slice.peer.compactDisplayTitle).string + let text = !isHidden ? component.strings.StoryFeed_TooltipArchive(component.slice.effectivePeer.compactDisplayTitle).string : component.strings.StoryFeed_TooltipUnarchive(component.slice.effectivePeer.compactDisplayTitle).string let tooltipScreen = TooltipScreen( context: component.context, account: component.context.account, sharedContext: component.context.sharedContext, text: .markdown(text: text), style: .customBlur(UIColor(rgb: 0x1c1c1c), 0.0), - icon: .peer(peer: component.slice.peer, isStory: true), + icon: .peer(peer: component.slice.effectivePeer, isStory: true), action: TooltipScreen.Action( title: component.strings.Undo_Undo, action: { - component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.peer.id, isHidden: isHidden) + component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.effectivePeer.id, isHidden: isHidden) } ), location: .bottom, @@ -6767,7 +6765,7 @@ public final class StoryItemSetContainerComponent: Component { }))) } - if case .user = component.slice.peer { + if case .user = component.slice.effectivePeer { items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextStealthMode, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in @@ -6797,7 +6795,7 @@ public final class StoryItemSetContainerComponent: Component { let statsController = component.context.sharedContext.makeStoryStatsController( context: component.context, updatedPresentationData: (presentationData, .single(presentationData)), - peerId: component.slice.peer.id, + peerId: component.slice.effectivePeer.id, storyId: component.slice.item.storyItem.id, storyItem: component.slice.item.storyItem, fromStory: true @@ -6822,7 +6820,7 @@ public final class StoryItemSetContainerComponent: Component { } } - if !component.slice.peer.isService { + if !component.slice.effectivePeer.isService { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_Report, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, a in @@ -6836,7 +6834,7 @@ public final class StoryItemSetContainerComponent: Component { parent: controller, contextController: c, backAction: { _ in }, - subject: .story(component.slice.peer.id, component.slice.item.storyItem.id), + subject: .story(component.slice.effectivePeer.id, component.slice.item.storyItem.id), options: options, passthrough: true, forceTheme: defaultDarkPresentationTheme, @@ -6851,7 +6849,7 @@ public final class StoryItemSetContainerComponent: Component { guard let self, let component = self.component, let controller = component.controller(), let reason else { return } - let _ = component.context.engine.peers.reportPeerStory(peerId: component.slice.peer.id, storyId: component.slice.item.storyItem.id, reason: reason, message: "").startStandalone() + let _ = component.context.engine.peers.reportPeerStory(peerId: component.slice.effectivePeer.id, storyId: component.slice.item.storyItem.id, reason: reason, message: "").startStandalone() controller.present( UndoOverlayController( presentationData: presentationData, @@ -6968,7 +6966,7 @@ public final class StoryItemSetContainerComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -7013,7 +7011,7 @@ final class ListContextExtractedContentSource: ContextExtractedContentSource { } -private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat, duration: Double, curve: Transition.Animation.Curve, reverse: Bool) -> [CGPoint] { +private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat, duration: Double, curve: ComponentTransition.Animation.Curve, reverse: Bool) -> [CGPoint] { let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation) let x1 = sourcePoint.x diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 8abf1537a3f..279e2bee0d4 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -199,7 +199,7 @@ final class StoryItemSetContainerSendMessage { }, requestLayout: { [weak self] transition in if let self { - self.view?.state?.updated(transition: Transition(transition)) + self.view?.state?.updated(transition: ComponentTransition(transition)) } } ) @@ -220,7 +220,7 @@ final class StoryItemSetContainerSendMessage { } } - func updateInputMediaNode(view: StoryItemSetContainerComponent.View, availableSize: CGSize, bottomInset: CGFloat, bottomContainerInset: CGFloat, inputHeight: CGFloat, effectiveInputHeight: CGFloat, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: Transition) -> CGFloat { + func updateInputMediaNode(view: StoryItemSetContainerComponent.View, availableSize: CGSize, bottomInset: CGFloat, bottomContainerInset: CGFloat, inputHeight: CGFloat, effectiveInputHeight: CGFloat, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: ComponentTransition) -> CGFloat { guard let context = self.context, let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else { return 0.0 } @@ -284,8 +284,8 @@ final class StoryItemSetContainerSendMessage { if self.needsInputActivation { let inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight) - Transition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) - Transition.immediate.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeFrame) + ComponentTransition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + ComponentTransition.immediate.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeFrame) } transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeFrame) @@ -571,7 +571,7 @@ final class StoryItemSetContainerSendMessage { guard let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else { return } - let peer = component.slice.peer + let peer = component.slice.effectivePeer let controller = component.controller() as? StoryContainerScreen @@ -584,12 +584,12 @@ final class StoryItemSetContainerSendMessage { let _ = enqueueMessages(account: component.context.account, peerId: peerId, messages: messages).start() - view.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } else if self.hasRecordedVideoPreview, let videoRecorderValue = self.videoRecorderValue { videoRecorderValue.send() self.hasRecordedVideoPreview = false self.videoRecorder.set(.single(nil)) - view.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } else { switch inputPanelView.getSendMessageInput() { case let .text(text): @@ -638,7 +638,7 @@ final class StoryItemSetContainerSendMessage { return } let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id) - let peer = component.slice.peer + let peer = component.slice.effectivePeer let controller = component.controller() as? StoryContainerScreen @@ -692,7 +692,7 @@ final class StoryItemSetContainerSendMessage { return } let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id) - let peer = component.slice.peer + let peer = component.slice.effectivePeer let controller = component.controller() as? StoryContainerScreen @@ -740,7 +740,7 @@ final class StoryItemSetContainerSendMessage { guard let component = view.component else { return } - let peer = component.slice.peer + let peer = component.slice.effectivePeer let _ = (legacyEnqueueGifMessage(account: component.context.account, data: data) |> deliverOnMainQueue).start(next: { [weak self, weak view] message in if let self, let view { self.sendMessages(view: view, peer: peer, messages: [message]) @@ -752,7 +752,7 @@ final class StoryItemSetContainerSendMessage { guard let component = view.component else { return } - let peer = component.slice.peer + let peer = component.slice.effectivePeer let size = image.size.aspectFitted(CGSize(width: 512.0, height: 512.0)) @@ -912,7 +912,7 @@ final class StoryItemSetContainerSendMessage { } self.hasRecordedVideoPreview = false - view.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } } }) @@ -940,13 +940,13 @@ final class StoryItemSetContainerSendMessage { component.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) self.recordedAudioPreview = .audio(ChatRecordedMediaPreview.Audio(resource: resource, fileSize: Int32(data.compressedData.count), duration: Int32(data.duration), waveform: AudioWaveform(bitstream: waveform, bitsPerSample: 5))) - view.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } }) } else if let videoRecorderValue = self.videoRecorderValue { if videoRecorderValue.stopVideo() { self.hasRecordedVideoPreview = true - view.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } else { self.videoRecorder.set(.single(nil)) } @@ -957,12 +957,12 @@ final class StoryItemSetContainerSendMessage { if self.recordedAudioPreview != nil { self.recordedAudioPreview = nil self.wasRecordingDismissed = true - view.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } else if self.hasRecordedVideoPreview { self.videoRecorder.set(.single(nil)) self.hasRecordedVideoPreview = false self.wasRecordingDismissed = true - view.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } } @@ -1013,7 +1013,7 @@ final class StoryItemSetContainerSendMessage { component.presentController(actionSheet, nil) } else { var preferredAction: ShareControllerPreferredAction? - if focusedItem.storyItem.isPublic && !component.slice.peer.isService { + if focusedItem.storyItem.isPublic && !component.slice.effectivePeer.isService { preferredAction = .custom(action: ShareControllerAction(title: component.strings.Story_Context_CopyLink, action: { let _ = ((component.context.engine.messages.exportStoryLink(peerId: peerId, id: focusedItem.storyItem.id)) |> deliverOnMainQueue).start(next: { link in @@ -1170,7 +1170,7 @@ final class StoryItemSetContainerSendMessage { guard let self, let view else { return } - let peer = component.slice.peer + let peer = component.slice.effectivePeer let _ = self @@ -1260,7 +1260,7 @@ final class StoryItemSetContainerSendMessage { return } - let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id) + let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.effectivePeer.id, id: component.slice.item.storyItem.id) |> deliverOnMainQueue).start(next: { [weak view] link in guard let view, let component = view.component else { return @@ -1808,7 +1808,7 @@ final class StoryItemSetContainerSendMessage { //TODO:gift controller break case let .app(bot): - let params = WebAppParameters(source: .attachMenu, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false) + let params = WebAppParameters(source: .attachMenu, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: true) let theme = component.theme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) let controller = WebAppController(context: component.context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil, threadId: nil) @@ -2214,7 +2214,7 @@ final class StoryItemSetContainerSendMessage { inputText = text } - let peer = component.slice.peer + let peer = component.slice.effectivePeer let theme = defaultDarkPresentationTheme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) let controller = mediaPasteboardScreen( @@ -2283,7 +2283,7 @@ final class StoryItemSetContainerSendMessage { return } let context = component.context - let peer = component.slice.peer + let peer = component.slice.effectivePeer let storyId = component.slice.item.storyItem.id let theme = component.theme @@ -2669,7 +2669,7 @@ final class StoryItemSetContainerSendMessage { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData)) - let peerId = component.slice.peer.id + let peerId = component.slice.effectivePeer.id component.context.sharedContext.openResolvedUrl( result, context: component.context, @@ -2851,7 +2851,7 @@ final class StoryItemSetContainerSendMessage { return } - let peerId = component.slice.peer.id + let peerId = component.slice.effectivePeer.id var resolveSignal: Signal if let peerName = peerName { @@ -2918,13 +2918,13 @@ final class StoryItemSetContainerSendMessage { return } if !hashtag.isEmpty { - /*if !"".isEmpty { - let searchController = component.context.sharedContext.makeStorySearchController(context: component.context, query: hashtag) + if peerName == nil { + let searchController = component.context.sharedContext.makeStorySearchController(context: component.context, scope: .query(hashtag), listContext: nil) navigationController.pushViewController(searchController) - } else {*/ + } else { let searchController = component.context.sharedContext.makeHashtagSearchController(context: component.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, all: true) navigationController.pushViewController(searchController) - //} + } } })) } @@ -3346,31 +3346,10 @@ final class StoryItemSetContainerSendMessage { var actions: [ContextMenuAction] = [] switch mediaArea { - case let .venue(_, venue): - let action = { [weak controller, weak view] in - let subject = EngineMessage(stableId: 0, stableVersion: 0, id: EngineMessage.Id(peerId: PeerId(0), namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [.geo(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: venue.venue, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil))], peers: [:], associatedMessages: [:], associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - let locationController = LocationViewController( - context: context, - updatedPresentationData: updatedPresentationData, - subject: subject, - isStoryLocation: true, - params: LocationViewParams( - sendLiveLocation: { _ in }, - stopLiveLocation: { _ in }, - openUrl: { url in - context.sharedContext.applicationBindings.openUrl(url) - }, - openPeer: { _ in } - ) - ) - view?.updateModalTransitionFactor(1.0, transition: .animated(duration: 0.5, curve: .spring)) - locationController.dismissed = { [weak view] in - view?.updateModalTransitionFactor(0.0, transition: .animated(duration: 0.5, curve: .spring)) - Queue.mainQueue().after(0.5, { - view?.updateIsProgressPaused() - }) - } - controller?.push(locationController) + case let .venue(coordinates, venue): + let action = { [weak controller] in + let searchController = context.sharedContext.makeStorySearchController(context: context, scope: .location(coordinates: coordinates, venue: venue), listContext: nil) + controller?.push(searchController) } if immediate { action() @@ -3431,6 +3410,30 @@ final class StoryItemSetContainerSendMessage { })) case .reaction: return + case let .link(_, url): + let action = { + let _ = openUserGeneratedUrl(context: component.context, peerId: component.slice.effectivePeer.id, url: url, concealed: false, skipUrlAuth: false, skipConcealedAlert: false, forceDark: true, present: { [weak controller] c in + controller?.present(c, in: .window(.root)) + }, openResolved: { [weak self, weak view] resolved in + guard let self, let view else { + return + } + self.openResolved(view: view, result: resolved, forceExternal: false, concealed: false) + }, alertDisplayUpdated: { [weak self, weak view] alertController in + guard let self, let view else { + return + } + self.statusController = alertController + view.updateIsProgressPaused() + }) + } + if immediate { + action() + return + } + actions.append(ContextMenuAction(content: .textWithSubtitleAndIcon(title: updatedPresentationData.initial.strings.Story_ViewLink, subtitle: url, icon: generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: .white)), action: { + action() + })) } self.selectedMediaArea = mediaArea @@ -3447,12 +3450,16 @@ final class StoryItemSetContainerSendMessage { let node = controller.displayNode let menuController = makeContextMenuController(actions: actions, blurred: true) menuController.centerHorizontally = true - menuController.dismissed = { [weak self, weak view] in + menuController.dismissed = { [weak self, weak view, weak menuController] in if let self, let view { - self.selectedMediaArea = nil - Queue.mainQueue().after(0.1) { - self.menuController = nil - view.updateIsProgressPaused() + if self.menuController === menuController { + self.selectedMediaArea = nil + Queue.mainQueue().after(0.1) { + if self.menuController === menuController { + self.menuController = nil + view.updateIsProgressPaused() + } + } } } } @@ -3485,8 +3492,8 @@ final class StoryItemSetContainerSendMessage { guard let view, let component = view.component else { return } - if component.slice.peer.id != component.context.account.peerId { - let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: reaction).start() + if component.slice.effectivePeer.id != component.context.account.peerId { + let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.effectivePeer.id, id: component.slice.item.storyItem.id, reaction: reaction).start() } let targetFrame = reactionView.convert(reactionView.bounds, to: view) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index 4693f3baf5d..29ea4a47263 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -431,7 +431,7 @@ final class StoryItemSetViewListComponent: Component { self.dismissInput?() } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let itemLayout = self.itemLayout else { return } @@ -860,7 +860,7 @@ final class StoryItemSetViewListComponent: Component { if applyState { //TODO:determine sync - self.state?.updated(transition: Transition.immediate.withUserData(PeerListItemComponent.TransitionHint(synchronousLoad: true))) + self.state?.updated(transition: ComponentTransition.immediate.withUserData(PeerListItemComponent.TransitionHint(synchronousLoad: true))) } var hasContent = false @@ -903,7 +903,7 @@ final class StoryItemSetViewListComponent: Component { } } - func update(component: StoryItemSetViewListComponent, availableSize: CGSize, visualHeight: CGFloat, sideInset: CGFloat, navigationHeight: CGFloat, navigationSearchPartHeight: CGFloat, isSearchActive: Bool, transition: Transition) { + func update(component: StoryItemSetViewListComponent, availableSize: CGSize, visualHeight: CGFloat, sideInset: CGFloat, navigationHeight: CGFloat, navigationSearchPartHeight: CGFloat, isSearchActive: Bool, transition: ComponentTransition) { let themeUpdated = self.component?.theme !== component.theme self.component = component @@ -1351,13 +1351,13 @@ final class StoryItemSetViewListComponent: Component { self.currentContentView?.sourceView(storyId: storyId) } - func animateIn(transition: Transition) { + func animateIn(transition: ComponentTransition) { let offset = self.bounds.height - self.navigationBarBackground.frame.minY - Transition.immediate.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset)) + ComponentTransition.immediate.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset)) transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: 0.0)) } - func animateOut(transition: Transition, completion: @escaping () -> Void) { + func animateOut(transition: ComponentTransition, completion: @escaping () -> Void) { let offset = self.bounds.height - self.navigationBarBackground.frame.minY transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset), completion: { _ in completion() @@ -1459,14 +1459,14 @@ final class StoryItemSetViewListComponent: Component { let _ = self if let sourceView { - let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)) transition.setAlpha(view: sourceView, alpha: 1.0) } } controller.present(contextController, in: .window(.root)) } - func update(component: StoryItemSetViewListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryItemSetViewListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme self.component = component @@ -1586,7 +1586,7 @@ final class StoryItemSetViewListComponent: Component { var tabSelectorTransition = transition if transition.animation.isImmediate, self.tabSelector.view != nil { - tabSelectorTransition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + tabSelectorTransition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring)) } let tabSelectorSize = self.tabSelector.update( transition: tabSelectorTransition, @@ -1901,7 +1901,7 @@ final class StoryItemSetViewListComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryPrivacyIconComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryPrivacyIconComponent.swift index e5a898bd937..0d61d90a46b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryPrivacyIconComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryPrivacyIconComponent.swift @@ -48,7 +48,7 @@ final class StoryPrivacyIconComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: StoryPrivacyIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryPrivacyIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousPrivacy = self.component?.privacy self.component = component self.state = state @@ -133,7 +133,7 @@ final class StoryPrivacyIconComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index 39d580dace1..4c5d2bbb501 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -252,7 +252,7 @@ public final class StoryFooterPanelComponent: Component { component.cancelUploadAction() } - func update(component: StoryFooterPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryFooterPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let isFirstTime = self.component == nil let previousComponent = self.component @@ -1046,7 +1046,7 @@ public final class StoryFooterPanelComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 0b79cb0ebb9..34039785689 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -564,7 +564,7 @@ public final class StoryPeerListComponent: Component { } } - private func updateScrolling(transition: Transition) { + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let itemLayout = self.itemLayout else { return } @@ -604,9 +604,9 @@ public final class StoryPeerListComponent: Component { self.titleIconView = titleIconView } - var titleIconTransition: Transition + var titleIconTransition: ComponentTransition if animateStatusTransition { - titleIconTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + titleIconTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) } else { titleIconTransition = .immediate } @@ -1479,7 +1479,7 @@ public final class StoryPeerListComponent: Component { return result } - func update(component: StoryPeerListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryPeerListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { var transition = transition transition.animation = .none @@ -1681,7 +1681,7 @@ public final class StoryPeerListComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index 1cea706b2c4..b4a488ae514 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -196,7 +196,7 @@ private final class StoryProgressLayer: HierarchyTrackingLayer { self.uploadProgressLayer.path = nil } - func updateAnimations(transition: Transition) { + func updateAnimations(transition: ComponentTransition) { guard let params = self.currentParams else { return } @@ -276,7 +276,7 @@ private final class StoryProgressLayer: HierarchyTrackingLayer { } } - func update(size: CGSize, lineWidth: CGFloat, radius: CGFloat, value: Value, transition: Transition) { + func update(size: CGSize, lineWidth: CGFloat, radius: CGFloat, value: Value, transition: ComponentTransition) { let params = Params( size: size, lineWidth: lineWidth, @@ -346,7 +346,7 @@ public final class StoryPeerListItemComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(state: StoryContainerScreen.TransitionState, transition: Transition) { + func update(state: StoryContainerScreen.TransitionState, transition: ComponentTransition) { let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress) if let snapshotView = self.snapshotView { @@ -635,7 +635,7 @@ public final class StoryPeerListItemComponent: Component { self.avatarBackgroundView.alpha = isPreviewing ? 0.0 : 1.0 } - func update(component: StoryPeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryPeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let size = availableSize let themeUpdated = self.component?.theme !== component.theme @@ -823,10 +823,10 @@ public final class StoryPeerListItemComponent: Component { } else if let mappedLeftCenter { avatarPath.addEllipse(in: CGRect(origin: CGPoint(), size: avatarSize).insetBy(dx: -indicatorLineSeenWidth * 1.4, dy: -indicatorLineSeenWidth * 1.4).offsetBy(dx: -abs(indicatorCenter.x - mappedLeftCenter.x), dy: -abs(indicatorCenter.y - mappedLeftCenter.y))) } - Transition.immediate.setShapeLayerPath(layer: self.avatarShapeLayer, path: avatarPath) + ComponentTransition.immediate.setShapeLayerPath(layer: self.avatarShapeLayer, path: avatarPath) - Transition.immediate.setShapeLayerPath(layer: self.indicatorShapeSeenLayer, path: calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, totalCount: component.totalCount, unseenCount: component.unseenCount, isSeen: true, segmentFraction: component.expandedAlphaFraction, rotationFraction: component.expandEffectFraction)) - Transition.immediate.setShapeLayerPath(layer: self.indicatorShapeUnseenLayer, path: calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, totalCount: component.totalCount, unseenCount: component.unseenCount, isSeen: false, segmentFraction: component.expandedAlphaFraction, rotationFraction: component.expandEffectFraction)) + ComponentTransition.immediate.setShapeLayerPath(layer: self.indicatorShapeSeenLayer, path: calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, totalCount: component.totalCount, unseenCount: component.unseenCount, isSeen: true, segmentFraction: component.expandedAlphaFraction, rotationFraction: component.expandEffectFraction)) + ComponentTransition.immediate.setShapeLayerPath(layer: self.indicatorShapeUnseenLayer, path: calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, totalCount: component.totalCount, unseenCount: component.unseenCount, isSeen: false, segmentFraction: component.expandedAlphaFraction, rotationFraction: component.expandEffectFraction)) let titleString: String if component.peer.id == component.context.account.peerId { @@ -908,7 +908,7 @@ public final class StoryPeerListItemComponent: Component { switch ringAnimation { case let .progress(progress): - let progressTransition: Transition + let progressTransition: ComponentTransition if abs(progress - 0.028) < 0.001 { progressTransition = .immediate } else { @@ -968,7 +968,7 @@ public final class StoryPeerListItemComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/TitleActivityIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/TitleActivityIndicatorComponent.swift index 01faaca5b8e..4b6c0ac4337 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/TitleActivityIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/TitleActivityIndicatorComponent.swift @@ -198,7 +198,7 @@ public final class TitleActivityIndicatorComponent: Component { } } - func update(component: TitleActivityIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: TitleActivityIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let isFirstTime = self.component == nil self.component = component let _ = isFirstTime @@ -219,7 +219,7 @@ public final class TitleActivityIndicatorComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/Sources/StoryQualityUpgradeSheetScreen.swift b/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/Sources/StoryQualityUpgradeSheetScreen.swift index 3db72ef0937..024a40960a6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/Sources/StoryQualityUpgradeSheetScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/Sources/StoryQualityUpgradeSheetScreen.swift @@ -107,7 +107,7 @@ private final class StoryQualityUpgradeSheetContentComponent: Component { deinit { } - func update(component: StoryQualityUpgradeSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryQualityUpgradeSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -265,7 +265,7 @@ private final class StoryQualityUpgradeSheetContentComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -306,7 +306,7 @@ private final class StoryQualityUpgradeSheetScreenComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: StoryQualityUpgradeSheetScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryQualityUpgradeSheetScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let environment = environment[ViewControllerComponentContainer.Environment.self].value @@ -379,7 +379,7 @@ private final class StoryQualityUpgradeSheetScreenComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift index 6bb68083046..2ff507bc337 100644 --- a/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift @@ -8,6 +8,7 @@ import Postbox import SwiftSignalKit import AccountContext import PhotoResources +import AvatarNode private final class ShapeImageView: UIView { struct Item: Equatable { @@ -88,10 +89,37 @@ private final class ShapeImageView: UIView { } public final class StorySetIndicatorComponent: Component { + public final class Item: Equatable { + public let storyItem: EngineStoryItem + public let peer: EnginePeer + + public init(storyItem: EngineStoryItem, peer: EnginePeer) { + self.storyItem = storyItem + self.peer = peer + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs === rhs { + return true + } + if lhs.storyItem != rhs.storyItem { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + var id: String { + return "\(self.peer.id.toInt64())_\(self.storyItem.id)" + } + } + public let context: AccountContext public let strings: PresentationStrings - public let peer: EnginePeer - public let items: [EngineStoryItem] + public let items: [Item] + public let displayAvatars: Bool public let hasUnseen: Bool public let hasUnseenPrivate: Bool public let totalCount: Int @@ -101,8 +129,8 @@ public final class StorySetIndicatorComponent: Component { public init( context: AccountContext, strings: PresentationStrings, - peer: EnginePeer, - items: [EngineStoryItem], + items: [Item], + displayAvatars: Bool, hasUnseen: Bool, hasUnseenPrivate: Bool, totalCount: Int, @@ -111,8 +139,8 @@ public final class StorySetIndicatorComponent: Component { ) { self.context = context self.strings = strings - self.peer = peer self.items = items + self.displayAvatars = displayAvatars self.hasUnseen = hasUnseen self.hasUnseenPrivate = hasUnseenPrivate self.totalCount = totalCount @@ -127,6 +155,9 @@ public final class StorySetIndicatorComponent: Component { if lhs.items != rhs.items { return false } + if lhs.displayAvatars != rhs.displayAvatars { + return false + } if lhs.hasUnseen != rhs.hasUnseen { return false } @@ -149,13 +180,13 @@ public final class StorySetIndicatorComponent: Component { private(set) var image: UIImage? - init(context: AccountContext, peer: EnginePeer, item: EngineStoryItem, updated: @escaping () -> Void) { + init(context: AccountContext, item: StorySetIndicatorComponent.Item, displayAvatars: Bool, updated: @escaping () -> Void) { self.updated = updated - let peerReference = PeerReference(peer._asPeer()) + let peerReference = PeerReference(item.peer._asPeer()) var messageMedia: EngineMedia? - switch item.media { + switch item.storyItem.media { case let .image(image): messageMedia = .image(image) case let .file(file): @@ -167,60 +198,82 @@ public final class StorySetIndicatorComponent: Component { let reloadMedia = true if reloadMedia, let messageMedia, let peerReference { + var imageSignal: Signal? var signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + var fetchSignal: Signal? - switch messageMedia { - case let .image(image): - signal = chatMessagePhoto( - postbox: context.account.postbox, - userLocation: .peer(peerReference.id), - userContentType: .story, - photoReference: .story(peer: peerReference, id: item.id, media: image), - synchronousLoad: false, - highQuality: true - ) - if let representation = image.representations.last { + + if displayAvatars { + imageSignal = peerAvatarCompleteImage(postbox: context.account.postbox, network: context.account.network, peer: item.peer, forceProvidedRepresentation: false, representation: nil, size: CGSize(width: 26.0, height: 26.0), round: true, font: avatarPlaceholderFont(size: 13.0), drawLetters: true, fullSize: false, blurred: false) + } else { + switch messageMedia { + case let .image(image): + signal = chatMessagePhoto( + postbox: context.account.postbox, + userLocation: .peer(peerReference.id), + userContentType: .story, + photoReference: .story(peer: peerReference, id: item.storyItem.id, media: image), + synchronousLoad: false, + highQuality: true + ) + if let representation = image.representations.last { + fetchSignal = fetchedMediaResource( + mediaBox: context.account.postbox.mediaBox, + userLocation: .peer(peerReference.id), + userContentType: .story, + reference: ImageMediaReference.story(peer: peerReference, id: item.storyItem.id, media: image).resourceReference(representation.resource) + ) + |> ignoreValues + |> `catch` { _ -> Signal in + return .complete() + } + } + case let .file(file): + signal = mediaGridMessageVideo( + postbox: context.account.postbox, + userLocation: .peer(peerReference.id), + userContentType: .story, + videoReference: .story(peer: peerReference, id: item.storyItem.id, media: file), + onlyFullSize: false, + useLargeThumbnail: true, + synchronousLoad: false, + autoFetchFullSizeThumbnail: true, + overlayColor: nil, + nilForEmptyResult: false, + useMiniThumbnailIfAvailable: false, + blurred: false + ) fetchSignal = fetchedMediaResource( mediaBox: context.account.postbox.mediaBox, userLocation: .peer(peerReference.id), userContentType: .story, - reference: ImageMediaReference.story(peer: peerReference, id: item.id, media: image).resourceReference(representation.resource) + reference: FileMediaReference.story(peer: peerReference, id: item.storyItem.id, media: file).resourceReference(file.resource) ) |> ignoreValues |> `catch` { _ -> Signal in return .complete() } + default: + break } - case let .file(file): - signal = mediaGridMessageVideo( - postbox: context.account.postbox, - userLocation: .peer(peerReference.id), - userContentType: .story, - videoReference: .story(peer: peerReference, id: item.id, media: file), - onlyFullSize: false, - useLargeThumbnail: true, - synchronousLoad: false, - autoFetchFullSizeThumbnail: true, - overlayColor: nil, - nilForEmptyResult: false, - useMiniThumbnailIfAvailable: false, - blurred: false - ) - fetchSignal = fetchedMediaResource( - mediaBox: context.account.postbox.mediaBox, - userLocation: .peer(peerReference.id), - userContentType: .story, - reference: FileMediaReference.story(peer: peerReference, id: item.id, media: file).resourceReference(file.resource) - ) - |> ignoreValues - |> `catch` { _ -> Signal in - return .complete() - } - default: - break } - if let signal { + if let imageSignal { + var wasSynchronous = true + self.imageDisposable = (imageSignal + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + if let result { + self.image = result + if !wasSynchronous { + self.updated() + } + } + }) + wasSynchronous = false + } else if let signal { var wasSynchronous = true self.imageDisposable = (signal |> deliverOnMainQueue).start(next: { [weak self] process in @@ -261,7 +314,7 @@ public final class StorySetIndicatorComponent: Component { private let imageView: ShapeImageView private let text = ComponentView() - private var imageContexts: [Int32: ImageContext] = [:] + private var imageContexts: [String: ImageContext] = [:] private var component: StorySetIndicatorComponent? private weak var state: EmptyComponentState? @@ -291,10 +344,10 @@ public final class StorySetIndicatorComponent: Component { return } if highlighted { - let transition = Transition(animation: .curve(duration: 0.16, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.16, curve: .easeInOut)) transition.setSublayerTransform(view: self.button, transform: CATransform3DMakeScale(0.8, 0.8, 1.0)) } else { - let transition = Transition(animation: .curve(duration: 0.24, curve: .easeInOut)) + let transition = ComponentTransition(animation: .curve(duration: 0.24, curve: .easeInOut)) transition.setSublayerTransform(view: self.button, transform: CATransform3DIdentity) } } @@ -308,7 +361,7 @@ public final class StorySetIndicatorComponent: Component { self.component?.action() } - func update(component: StorySetIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StorySetIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -318,7 +371,7 @@ public final class StorySetIndicatorComponent: Component { let outerDiameter: CGFloat = innerDiameter + innerSpacing * 2.0 + lineWidth * 2.0 let overflow: CGFloat = 14.0 - var validIds: [Int32] = [] + var validIds: [String] = [] var items: [ShapeImageView.Item] = [] for i in 0 ..< min(3, component.items.count) { validIds.append(component.items[i].id) @@ -328,7 +381,7 @@ public final class StorySetIndicatorComponent: Component { imageContext = current } else { var update = false - imageContext = ImageContext(context: component.context, peer: component.peer, item: component.items[i], updated: { [weak self] in + imageContext = ImageContext(context: component.context, item: component.items[i], displayAvatars: component.displayAvatars, updated: { [weak self] in guard let self else { return } @@ -347,7 +400,7 @@ public final class StorySetIndicatorComponent: Component { )) } - var removeIds: [Int32] = [] + var removeIds: [String] = [] for (id, _) in self.imageContexts { if !validIds.contains(id) { removeIds.append(id) @@ -407,7 +460,12 @@ public final class StorySetIndicatorComponent: Component { textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) } - let size = CGSize(width: effectiveItemsWidth + 6.0 + textSize.width, height: outerDiameter) + var width = effectiveItemsWidth + if textSize.width > 0.0 { + width += textSize.width + 6.0 + } + + let size = CGSize(width: width, height: outerDiameter) transition.setFrame(view: self.button, frame: CGRect(origin: CGPoint(), size: size)) return size @@ -418,7 +476,7 @@ public final class StorySetIndicatorComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeInfoContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeInfoContentComponent.swift index 38cbd156f7e..dae2462be19 100644 --- a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeInfoContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeInfoContentComponent.swift @@ -111,7 +111,7 @@ public final class StoryStealthModeInfoContentComponent: Component { } } - func update(component: StoryStealthModeInfoContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryStealthModeInfoContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let sideInset: CGFloat = 16.0 @@ -313,7 +313,7 @@ public final class StoryStealthModeInfoContentComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift index 61ea6561a80..ba418b15456 100644 --- a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift @@ -78,7 +78,7 @@ private final class StoryStealthModeSheetContentComponent: Component { private func displayCooldown() { if !self.showCooldownToast { self.showCooldownToast = true - self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))) } self.hideCooldownTimer?.invalidate() @@ -91,11 +91,11 @@ private final class StoryStealthModeSheetContentComponent: Component { self.showCooldownToast = false - self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))) }) } - func update(component: StoryStealthModeSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryStealthModeSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -326,7 +326,7 @@ private final class StoryStealthModeSheetContentComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -385,7 +385,7 @@ private final class StoryStealthModeSheetScreenComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: StoryStealthModeSheetScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: StoryStealthModeSheetScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let environment = environment[ViewControllerComponentContainer.Environment.self].value @@ -461,7 +461,7 @@ private final class StoryStealthModeSheetScreenComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/SwitchComponent/Sources/SwitchComponent.swift b/submodules/TelegramUI/Components/SwitchComponent/Sources/SwitchComponent.swift index 305628999ba..8858bdaecd3 100644 --- a/submodules/TelegramUI/Components/SwitchComponent/Sources/SwitchComponent.swift +++ b/submodules/TelegramUI/Components/SwitchComponent/Sources/SwitchComponent.swift @@ -55,7 +55,7 @@ public final class SwitchComponent: Component { self.component?.valueUpdated(self.switchView.isOn) } - func update(component: SwitchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: SwitchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.switchView.tintColor = component.tintColor @@ -72,7 +72,7 @@ public final class SwitchComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift index d54666377c2..2a2536c59cc 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift +++ b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift @@ -114,7 +114,7 @@ public final class TabSelectorComponent: Component { deinit { } - func update(component: TabSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: TabSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let selectionColorUpdated = component.colors.selection != self.component?.colors.selection self.component = component @@ -285,7 +285,7 @@ public final class TabSelectorComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index b9831240c97..12a9161dfd1 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -300,6 +300,59 @@ public final class TextFieldComponent: Component { NSAttributedString.Key.font: Font.regular(17.0), NSAttributedString.Key.foregroundColor: UIColor.white ] + + self.textView.toggleQuoteCollapse = { [weak self] range in + guard let self else { + return + } + + self.updateInputState { current in + let result = NSMutableAttributedString(attributedString: current.inputText) + var selectionRange = current.selectionRange + + if let _ = result.attribute(ChatTextInputAttributes.block, at: range.lowerBound, effectiveRange: nil) as? ChatTextInputTextQuoteAttribute { + let blockString = NSMutableAttributedString(attributedString: result.attributedSubstring(from: range)) + blockString.removeAttribute(ChatTextInputAttributes.block, range: NSRange(location: 0, length: blockString.length)) + + result.replaceCharacters(in: range, with: "") + result.insert(NSAttributedString(string: " ", attributes: [ + ChatTextInputAttributes.collapsedBlock: blockString + ]), at: range.lowerBound) + + if selectionRange.lowerBound >= range.lowerBound && selectionRange.upperBound < range.upperBound { + selectionRange = range.lowerBound ..< range.lowerBound + } else if selectionRange.lowerBound >= range.upperBound { + let deltaLength = 1 - range.length + selectionRange = (selectionRange.lowerBound + deltaLength) ..< (selectionRange.lowerBound + deltaLength) + } + } else if let current = result.attribute(ChatTextInputAttributes.collapsedBlock, at: range.lowerBound, effectiveRange: nil) as? NSAttributedString { + result.replaceCharacters(in: range, with: "") + + let updatedBlockString = NSMutableAttributedString(attributedString: current) + updatedBlockString.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false), range: NSRange(location: 0, length: updatedBlockString.length)) + + result.insert(updatedBlockString, at: range.lowerBound) + + if selectionRange.lowerBound >= range.upperBound { + let deltaLength = updatedBlockString.length - 1 + selectionRange = (selectionRange.lowerBound + deltaLength) ..< (selectionRange.lowerBound + deltaLength) + } + } + + let stateResult = stateAttributedStringForText(result) + if selectionRange.lowerBound < 0 { + selectionRange = 0 ..< selectionRange.upperBound + } + if selectionRange.upperBound > stateResult.length { + selectionRange = selectionRange.lowerBound ..< stateResult.length + } + + return InputState( + inputText: stateResult, + selectionRange: selectionRange + ) + } + } } required init?(coder: NSCoder) { @@ -329,7 +382,7 @@ public final class TextFieldComponent: Component { self.updateEntities() if currentAttributedText != updatedAttributedText && !self.isUpdating { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textChanged))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textChanged))) } } @@ -349,7 +402,7 @@ public final class TextFieldComponent: Component { return state.insertText(text) } if !self.isUpdating { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textChanged))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textChanged))) } } @@ -362,7 +415,7 @@ public final class TextFieldComponent: Component { return TextFieldComponent.InputState(inputText: text, selectionRange: selectionRange) } if !self.isUpdating { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textChanged))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textChanged))) } } @@ -396,7 +449,7 @@ public final class TextFieldComponent: Component { } } if !self.isUpdating { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textChanged))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textChanged))) } component.paste(.text) return false @@ -469,7 +522,7 @@ public final class TextFieldComponent: Component { self.updateEntities() if !self.isUpdating { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textChanged))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textChanged))) } } @@ -498,7 +551,7 @@ public final class TextFieldComponent: Component { return } if !self.isUpdating { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textFocusChanged(isFocused: true)))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textFocusChanged(isFocused: true)))) } if component.isOneLineWhenUnfocused { Queue.mainQueue().justDispatch { @@ -509,7 +562,7 @@ public final class TextFieldComponent: Component { public func chatInputTextNodeDidFinishEditing() { if !self.isUpdating { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textFocusChanged(isFocused: false)))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textFocusChanged(isFocused: false)))) } } @@ -877,11 +930,17 @@ public final class TextFieldComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: component.theme) let updatedPresentationData: (initial: PresentationData, signal: Signal) = (presentationData, .single(presentationData)) - let controller = chatTextLinkEditController(sharedContext: component.context.sharedContext, updatedPresentationData: updatedPresentationData, account: component.context.account, text: text.string, link: link, apply: { [weak self] link in + let controller = chatTextLinkEditController(sharedContext: component.context.sharedContext, updatedPresentationData: updatedPresentationData, account: component.context.account, text: text.string, link: link, allowEmpty: true, apply: { [weak self] link in if let self { - if let link = link { - self.updateInputState { state in - return state.addLinkAttribute(selectionRange: selectionRange, url: link) + if let link { + if !link.isEmpty { + self.updateInputState { state in + return state.addLinkAttribute(selectionRange: selectionRange, url: link) + } + } else { + self.updateInputState { state in + return state.removeLinkAttribute(selectionRange: selectionRange) + } } self.textView.becomeFirstResponder() } @@ -900,7 +959,7 @@ public final class TextFieldComponent: Component { public func getAttributedText() -> NSAttributedString { Keyboard.applyAutocorrection(textView: self.textView) - return self.inputState.inputText + return expandedInputStateAttributedString(self.inputState.inputText) } public func setAttributedText(_ string: NSAttributedString, updateState: Bool) { @@ -908,7 +967,7 @@ public final class TextFieldComponent: Component { return TextFieldComponent.InputState(inputText: string, selectionRange: string.length ..< string.length) } if updateState && !self.isUpdating { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textChanged))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textChanged))) } } @@ -991,7 +1050,7 @@ public final class TextFieldComponent: Component { if let spoilerView = self.spoilerView { if animated { - let transition = Transition.easeInOut(duration: 0.3) + let transition = ComponentTransition.easeInOut(duration: 0.3) if revealed { transition.setAlpha(view: spoilerView, alpha: 0.0) } else { @@ -1108,7 +1167,7 @@ public final class TextFieldComponent: Component { } } - public func updateEmojiSuggestion(transition: Transition) { + public func updateEmojiSuggestion(transition: ComponentTransition) { guard let component = self.component else { return } @@ -1194,7 +1253,7 @@ public final class TextFieldComponent: Component { return CGPoint(x: rightmostX, y: rightmostY) } - func update(component: TextFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: TextFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -1352,7 +1411,7 @@ public final class TextFieldComponent: Component { let hasMoreThanOneLine = ellipsisFrame.maxY < self.textView.contentSize.height - 12.0 - let ellipsisTransition: Transition + let ellipsisTransition: ComponentTransition if isEditing { ellipsisTransition = .easeInOut(duration: 0.2) } else { @@ -1376,7 +1435,7 @@ public final class TextFieldComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -1532,4 +1591,28 @@ extension TextFieldComponent.InputState { return self } } + + public func removeLinkAttribute(selectionRange: Range) -> TextFieldComponent.InputState { + if !selectionRange.isEmpty { + let nsRange = NSRange(location: selectionRange.lowerBound, length: selectionRange.count) + var attributesToRemove: [(NSAttributedString.Key, NSRange)] = [] + self.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in + for (key, _) in attributes { + if key == ChatTextInputAttributes.textUrl { + attributesToRemove.append((key, range)) + } else { + attributesToRemove.append((key, nsRange)) + } + } + } + + let result = NSMutableAttributedString(attributedString: self.inputText) + for (attribute, range) in attributesToRemove { + result.removeAttribute(attribute, range: range) + } + return TextFieldComponent.InputState(inputText: result, selectionRange: selectionRange) + } else { + return self + } + } } diff --git a/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift b/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift index 3d355dcc883..96b8035f7bf 100644 --- a/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift +++ b/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift @@ -51,7 +51,7 @@ public final class ToastContentComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ToastContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ToastContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { var contentHeight: CGFloat = 0.0 let leftInset: CGFloat = 9.0 @@ -100,7 +100,7 @@ public final class ToastContentComponent: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/TokenListTextField/Sources/TokenListTextField.swift b/submodules/TelegramUI/Components/TokenListTextField/Sources/TokenListTextField.swift index d46862dac97..8212f089a5b 100644 --- a/submodules/TelegramUI/Components/TokenListTextField/Sources/TokenListTextField.swift +++ b/submodules/TelegramUI/Components/TokenListTextField/Sources/TokenListTextField.swift @@ -163,7 +163,7 @@ public final class TokenListTextField: Component { } } - func update(component: TokenListTextField, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: TokenListTextField, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.componentState = state @@ -195,7 +195,7 @@ public final class TokenListTextField: Component { return } self.component?.isFocusedUpdated(self.tokenListNode?.isFocused ?? false) - self.componentState?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + self.componentState?.updated(transition: ComponentTransition(animation: .curve(duration: 0.35, curve: .spring))) } tokenListNode.textUpdated = { [weak self] text in @@ -261,7 +261,7 @@ public final class TokenListTextField: Component { return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/LoadingEffectView.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/LoadingEffectView.swift index 09d0508eb63..565032002d9 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/LoadingEffectView.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/LoadingEffectView.swift @@ -94,7 +94,7 @@ final class LoadingEffectView: UIView { self.borderGradientView.layer.add(animation, forKey: "shimmer") } - func update(size: CGSize, transition: Transition) { + func update(size: CGSize, transition: ComponentTransition) { if self.backgroundView.bounds.size != size { self.backgroundView.layer.removeAllAnimations() diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift index 7a56391afb1..b5c56fcbc31 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift @@ -1090,13 +1090,13 @@ public class VideoMessageCameraScreen: ViewController { }, transition: .easeInOut(duration: 0.2)) } - func requestUpdateLayout(transition: Transition) { + func requestUpdateLayout(transition: ComponentTransition) { if let layout = self.validLayout { self.containerLayoutUpdated(layout: layout, forceUpdate: true, transition: transition) } } - func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, transition: Transition) { + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, transition: ComponentTransition) { guard let controller = self.controller else { return } @@ -1306,13 +1306,13 @@ public class VideoMessageCameraScreen: ViewController { return self.node.cameraState } - fileprivate func updateCameraState(_ f: (CameraState) -> CameraState, transition: Transition) { + fileprivate func updateCameraState(_ f: (CameraState) -> CameraState, transition: ComponentTransition) { self.node.cameraState = f(self.node.cameraState) self.node.requestUpdateLayout(transition: transition) self.durationValue.set(self.cameraState.duration) } - fileprivate func updatePreviewState(_ f: (PreviewState?) -> PreviewState?, transition: Transition) { + fileprivate func updatePreviewState(_ f: (PreviewState?) -> PreviewState?, transition: ComponentTransition) { self.node.previewState = f(self.node.previewState) self.node.requestUpdateLayout(transition: transition) } @@ -1746,7 +1746,7 @@ public class VideoMessageCameraScreen: ViewController { super.containerLayoutUpdated(layout, transition: transition) if !self.isDismissed { - (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) } } @@ -1852,7 +1852,7 @@ private final class VideoMessageSendMessageContextPreview: UIView, ChatSendMessa preconditionFailure() } - func animateIn(transition: Transition) { + func animateIn(transition: ComponentTransition) { self.addSubview(self.previewContainerContentView) guard let previewContainerContentParentView = self.previewContainerContentParentView else { @@ -1865,7 +1865,7 @@ private final class VideoMessageSendMessageContextPreview: UIView, ChatSendMessa transition.animatePosition(view: self.previewContainerContentView, from: CGPoint(x: fromFrame.midX - toFrame.midX, y: fromFrame.midY - toFrame.midY), to: CGPoint(), additive: true) } - func animateOut(transition: Transition) { + func animateOut(transition: ComponentTransition) { guard let previewContainerContentParentView = self.previewContainerContentParentView else { return } @@ -1883,7 +1883,7 @@ private final class VideoMessageSendMessageContextPreview: UIView, ChatSendMessa }) } - func animateOutOnSend(transition: Transition) { + func animateOutOnSend(transition: ComponentTransition) { guard let previewContainerContentParentView = self.previewContainerContentParentView else { return } @@ -1897,7 +1897,7 @@ private final class VideoMessageSendMessageContextPreview: UIView, ChatSendMessa previewContainerContentParentView.addSubview(self.previewContainerContentView) } - func update(containerSize: CGSize, transition: Transition) -> CGSize { + func update(containerSize: CGSize, transition: ComponentTransition) -> CGSize { return self.previewContainerContentView.bounds.size } } diff --git a/submodules/TelegramUI/Images.xcassets/Components/Search Bar/Cashtag.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Components/Search Bar/Cashtag.imageset/Contents.json new file mode 100644 index 00000000000..297ddbe4b00 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Components/Search Bar/Cashtag.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "cash_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Components/Search Bar/Cashtag.imageset/cash_24.pdf b/submodules/TelegramUI/Images.xcassets/Components/Search Bar/Cashtag.imageset/cash_24.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a0e02c51e2c8394618f51b5d16875c6a6aa0e06e GIT binary patch literal 1264 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f-!Z<-99lzp0@KlyDZHnE}i0J z87ka=)qBR8iz#Kon>(++k8WGPp|Eepa{E17T9@meudknf{{H?tE4%+3{;%KHaMv>( zHQA?ZD<)t2beq`J;H;{G6urHd3iqu!@n1audA7#HRX6xeS(duItzZ;6xAPnCw28mJ zc^hQi4d@m1Tp_3KBp;xZe)Y_8(X5a!8M z(`T1+qp{uI#M-j?$d$;bxHD19yfkyqST`Crcm7f;Rk_d?b1p^3viwO=n~?@vu;7IN zA0I6iE=R>I%jar~#rAe9P2X&CtNZ7!vb4LpvJ;CV-g7f9y~m`!yp-XcPj{io{D-E- zxhe0`3n$OiV{2o}cF}V>CK4sdSa7giMD)8~so>7ynQZAR#8tAJr|0s9mQLY_`+n_k zy3p%wJ0G{4+jDi^$&=gWZx-6=`H<(}Hibq%gmRi@Nb3IquuLhoW`nHm_T^ zY=6L5n3f&q`a#BTL8RyVgHGu`MCcng_HIl!;)10f|LGF$Gho5K>kH3Bj|VzISE{ z(5VU_9|S4D>~YR71?n}#aDOo*%fSM}48Ql_d%qBnW?Fogjb^GB?B&G6p&fRh6ZIC5Dg%FfO60 zN{SLQb5e`AK(XiP0t^m~;{4oHO$Ck26irB==m+KJmneWd3=UxZ;LNI2pzFcqPhwFC Q*gJ*>CPrMUs;>TS0DIB9*#H0l literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Link.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Link.imageset/Contents.json new file mode 100644 index 00000000000..942d26181dd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Link.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "link.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Link.imageset/link.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Link.imageset/link.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4b654b898e4dc78f7e6ff9b0484729f1505f7ca5 GIT binary patch literal 1419 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f-!aK={zw*p4RW5MK-%B{SdPL zCN?30x&H$<%O!u+kk04ND~!xn-Zl9vbo{vbw=ajE%m1&ludDg}sqpWY+Mfk~zgEQl z)4P-LJMv}39{cd;a^_)ww%vN2ayYg2Pvp7ceDU@D(_2?|wys+Hrnl{L$Jg_R?&!?) z376ZUt#tg;}?8Vkt^Bdl|dHcmJ-R%DfmvoK@uhyP5`^%X`(Y5Sn zneDtU%&XZl=lc`Ik1B;zirDO?9Y~#W=in}$)-;*v5B(I*8CFf4cu+|}(Ii{qERQ!w zyQ4v{g!WLITKSG+4WVf= zN0LLVe`T$;mSUAHRhk>dc-Kj>$|=O?X7S4RxwCTOh1TDcxUF8HWRlS<;AN%6TKfK& z^3gN9WS_4&cW=qn@G5lyYwaCJV^bgg;xyd8!v2b(v8-F7#xhl5Nh$8d&6CatSZm#Q z=e+#!e71zXb2HRU7#2<{RF7ulDq`C=w{6zM`R}JqfBO29{2S*89#F=CrhHfy0_6!v z)-pFWgl8-hxF|5g={x4-<(CvIM8`t2Q%Ge&s)Bw%Vmd5;`KG31COYL;C`2n5=ox?k zf?)&~La<;a1*I0}mlh?b7At_VI4Ey|va54`UP)>m&_+=Hh6x5F76HW+Orb(Zr2t3> zUOMP|XQlw1ssQpqkOIse=loKjUPBD`7eh(|SYVi;m;~}*G2B9s!yq1aPAo~x$xlbs zT2Yjm#$}*j&IR#4h*U5$H8nO>0188ap@9OJrH}^~G6uR61Q0?NW|kO2mcYD9+DK)l|^POwoh{ihfXjeu)Ce!{7kc56-Mg Z1-c$wG$t06fW2dAXlc%+s_N?R1^}-m0DAxc literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/LinkLocked.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/LinkLocked.imageset/Contents.json new file mode 100644 index 00000000000..cd6a1f91514 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/LinkLocked.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "linklocked.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/LinkLocked.imageset/linklocked.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/LinkLocked.imageset/linklocked.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2c59d8d3c259a1f90f9d7927272868847f91f2ac GIT binary patch literal 1886 zcmZXV2UHVT6ozq8fgqw-Xwe;wV324EhA4}HAfSmf0|J65AsJ#KB$^Bzq)2EMipqv! zSwu8SS(<=~BB%j^vdT&mv9VZy4Hk&Vy6yx75AHp4&b;|&{`>B|bI$jSTx{*v;@6Q7 z2t0rT_x%q%oD^2R%BHZSOO(wXLH5QOL)oPY;egAAKp8 zCJvO0kH2eOalSa{a_c=sMAc1d`dz+^U)#OE_KDT9@t&_FxuP}EQ&R4oLwZL~l!>dH z6DtS~1u^p@@9DoZrepHGBJOav=M;4v9{D0R9CRKY(lhe%;Q8FB91Zs7MLrvz6JFL; zplFZ{cV9N{+!vTv<$dUwEUXMEQika*F9~`Zf(9>LfZqvhAy!LMjtuvGIC-fz^(H~^aMw|E6J=Rg9Jhn8xxWuu z+rOy0Mu@sED(ZaJiVpR4LaP6}j6fU%pML*l{PbCHdw zX%*R>wrjI2Kcpb76IzUada+}H^~$L8>!Px@o%|@uQ#(O1a9XYIRz}^OW>MiP21ERc zDlm{h+^RRyd{p=6HjLE%!qs$**G?U8MUDEYyMq;|L>KWW1#wO5ou}|ke3{62q!yzw z)MD#D61%-g?Ne~)2YS4`>$KA>T{GzB3dEPBXJtM*M&~Y9)s#5v4@3+ZG-jTMgD_icC>~58VyP(RU@{t~P3g4m! z+ukg`8^=o80%ma7%^I8L)Ylqp49fhyUM-m$^haLqL!@7pMLcaSrDt#cqw9}^J$~xR z{l&H(zplzcopWg|)Jc2qQqkwo(A=Q^G-J&o?<~u%&X!n8R}$)wBPV#-Q8nMx@C5&Y zUnCjX^X2)@_`P;y3r=0C;O)0i}M;MJT zHZNLoUu#b$T2mnnk-mEA(c7wRB^zoD`JBs66$!?gXw>(IMLJ_m&0#WZd(hj*t>}_R zUJ_(1MRR@LszjDe=pl6J&eNI}q3(TE3o~RdT29H9>(S00`#4~zx06nnTFc$IzD*04 z`aFp%K1dNqLYiUJJt~;+^3sHLdsGVNU450-q_up#M%-%6#x~KP;i!=81;CPu(p+t~ zQ#C>c8z|`|m2J$Ysd(yk<}`oWDESG}&=?G^Wx_x82GWgQ48_;`N=+G?axVjFft5q48;F z&wTzXN}AjFB27O6>W)6_XCPD}?i-_~V5g+lBrNfG5^0)&>!)QULn%BS9})uIKHtda z7PcP*ur3Vt6bc| zKpwoK62()Z3qt^F0P&jy<94bCrSu*6+c+$^R6yAQ9hll@Gd~FS;{R8_a1z{8napQo zQZ^`@-cVWCWaFC|5F>!ko@osgfc^*^KtfEmKXJ5}6NzR-0G7W69-hj?3*NLug6o_i z;mu&nj}mdij}p-g9@tEyIqtg=5->Ocpa7x#dsJI^9B3gw5HtbM9G1!CP_T499|DvO fPYf9A$_WSI>L*Syh5(vq2XAh^9${o;=WPEUd^7kp literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Grid/GroupingOff.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Grid/GroupingOff.imageset/Contents.json new file mode 100644 index 00000000000..9a5e75bd65d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Grid/GroupingOff.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "albumoff_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Grid/GroupingOff.imageset/albumoff_24.pdf b/submodules/TelegramUI/Images.xcassets/Media Grid/GroupingOff.imageset/albumoff_24.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3cd6c8100e17f6f062033adb618ccac66cd2a422 GIT binary patch literal 2114 zcmZXVc{H2(8pm5oNotv*I%Ig&rO`7)BB4y|u`daYt&|h;HW4Hmsj+nQ+EN5Vi;lgf z7{*ph-Cnteg1gQ@Ao)VBYdlPpb6fYAr?qi^Jc3$EW z7r?|B<05cL-$UQcu9sapndnzzkfqmwWn|&Eey{Rm|Y~A;QbYf1g-nSyp;=j%|va2-&MG3qrJvUH$@Z?CDNVR7% zboXppTcWq=7dI3<-bHRs`Ev|khKMLphFX?6)8A81Ga5ANVYi;rrw5uMEZ}XInq1J$ zG5>s8Fr_-45P#m-HghN=G)vS1Vp#_ou}``$CNb_xo=os15;9 zVmp|!=pgCietk`2j?Qg4<$)G%!M^2e(2o(${i>292I@^TmRMOWowkeNacE>7M@D2DV@_y2o9b8(?^h_)qq6kZ9d=pvgg%9hW_v_xn(m=l+2KVJlc?rt2{%#aEnU~Y zq42^j(yeZIw=KH>Gt#e=Xe0csIdHg;LRe`}3qzvee9)*M>AGQPIRnVe`5(3PECeEs`3 zU2!Jl=CM3!_Rjl-$blkQZ2m?J$vA7P$OAdKmB1}g|~zw%K9h@pWXfFO`4hq7P?l6^@gj1a&b z(A3b}w@(KY`Vf6O9Y%5lgBaJDK41{QS0=us@Kwf?L1%$JK^)vBPO-e-??~V9-%QeqjfVo03>0e+K1q z*AOOn6`~2~Kn~jHe*tX_MhgS*@JB;x0{bU`euT7mb@J^9Au-y#H~Aa7$Sdq~q=V7@ zJG!X#$&tk*`TB!Q2>vK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f-!gU!M;#Kj<)Z$U7O7|PIKbU zxq4*lFAnu&lbyz$Gmkv~zWh6%ZD*P5ez6~o-wr=tZomKB^89!^x%vC1^H-Pen|8l7 zEBbXt{;}U%KJ{qr%np_~oOt$ejb7#3_{U#sG=q56%&tsVt+NrI;j*fzF~quON4rj1 z>~FVyJQp9goHIDM#c!+Q-yM#PyMD?fC|&Md;IQSqo#Tp)6Pv$y9)2>T^pRel-w#Cr zF^QLNa(1-pUNd}^;bdht(MwqB&#U8Cmu$T_jg4W=;_#0T=WbUIb$%mRTbZ%oR7=sX zE>*wFk1n*$Y>k)DF(_Cf!}#bIS0{t8`^9z+uRyJ$mbR%kO!6368g2RvbBm@0n!1Ve zGIEt^UsPH6FHC1f*ws$WoTRO@`FkT)9$dvG_-me*?8F|;KZ`g*9#6g5a%^!&=;rm; znR&k}U6`q|y5mKhkVxVCk4N>>3vcVFPg7YwOGmkga1D{(tk!iyJg}eRGzoyhuld$&q;x+@335U zJb5ogL+M^U>$|rqo#zgf{t{@*KD4`K$M=q9(zcV1<$F(il-F^s#z$}qySky|xK$4lMfjK;pnZQMXiBI1#FE78OSRpzVnzTYH3sM#I0}|6=Y0Wn^B{R_}zd|8e z!9dRd3=j-MBbbn(8H@!pDJZo#zqBYhwO9d^+(GFSlysf*^GZ_lfHs2CI!rJiu?Q%p zUFNWj{SYVi;m;~}*G2B9s!yq1a zPAo~x$xlbsT2Yjm#$}*j&IR#4h*U5$H8nO>0188ap@9OJrH}^~G6uR61Q0@IMnM0d z37MFq3t1Rpm}Oy!Fsq~}F*7H%hzk^Zo-V-P&?wH&P1RJ;$V}0M1d4u8etwAp$iv_O d)(_6CN(H(eTudYum4LltX===+s_N?R1^~#soT2~# literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Grid/Paid.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Grid/Paid.imageset/Contents.json new file mode 100644 index 00000000000..a1f89c5d89e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Grid/Paid.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "cash.circle_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Grid/Paid.imageset/cash.circle_24.pdf b/submodules/TelegramUI/Images.xcassets/Media Grid/Paid.imageset/cash.circle_24.pdf new file mode 100644 index 0000000000000000000000000000000000000000..65dfc2983221cef0736d806329f832e10fcf5ff3 GIT binary patch literal 1453 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/SendStarsPeerBadgeStarIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/SendStarsPeerBadgeStarIcon.imageset/Contents.json new file mode 100644 index 00000000000..4fa2af457d0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/SendStarsPeerBadgeStarIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "star8.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/SendStarsPeerBadgeStarIcon.imageset/star8.pdf b/submodules/TelegramUI/Images.xcassets/Premium/SendStarsPeerBadgeStarIcon.imageset/star8.pdf new file mode 100644 index 0000000000000000000000000000000000000000..585ada1d39c62f739a3832cf0657c6428cea2191 GIT binary patch literal 1701 zcmZWq30D(W6h16fjItD2tb!feB2oxR*aS*BRS?7lIjB`Y3Q-1#K!Qn-2pkk4KtvV+ zkxdqXvMI9f%Q@fo$NIwN(mQA7y!qaJ_kQ=jH{S%LB_=5~p%)bjjii}y<=q$@VdMoE=atToGt@*_i@ELI3p-H`PK=0Yi4xTgz9L@=3+vbIEF zagfo;X1wxGJ^?FNxaEZ{H;gLrwz>#?&X`Z-yo)ayoLPV55ofu;joWOsu%6xJ7^}>T zEi+>jQ{~UG$sNv%dxTpYe9rBs+!j|W=QzVp92EG17*De8x&Q`)IEa@yIT+z*Y_lgb z7#$Kuu5qNQ%7STb#&DIJT(z*9W1RfO4O2R{SEQyC(e~I9@j%JeK+Kw>U2jz3v$vOU zZs&jry?6zi7aDb(aYNf(Y^SsJXxVXPe1;E}Xk}yOISia(9Y#(%dW5m7H2FTp_`VEt zQLt9wH72~dk-=KFW7D0zSU-0TFZcp07~nKMnAj1C8jP|QOQA)0$KKKySM+hFO!zLv zvFV3U^)-wfMIVMzvEzjn^oJC2@i#Pb(X-q`m5pT#V6Ey8tn;ISeV5NX{$0oym#~fr*03}9GAENU6`ACubjRcaycOFE3nk}SSB);gXz@qBPl`L`>)?L;9@dPgm9p7g>7f)+ zKX6^<%LuW7?71m=y}(}YLM8gkeNHE0Np$%wtY=tX`OuR!=uPvzhsjdbs{PP`{z!}C zb=qL{QwzRBKAdv89h4hF+gYwkl^8yYx&*YlVf^POSMyP0@nD@_s2VfkOf{FF0b3rY z*ze5c`qCG=O%~NpE3G*48;G9`~D9gA&8 z)g8}3yQu7|!hyWl^Uuy0=E0rZxnzrCTdv3~k zI_+hHMaq)Yq3T`xGKfAv4hIKu(q)sO%=kz)Np|tGtCn5J5)68?tQYz0%69;!X^jGt zBKM)M^wAC->Za94Z?EKTEgFFd_GO+KKIzouD;q ziwyheE$xmZ{UH=Cc-rj?6dfk2{tk-x-be^p~VUS literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/SendStarsStarSliderIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/SendStarsStarSliderIcon.imageset/Contents.json new file mode 100644 index 00000000000..17da40a8ad0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/SendStarsStarSliderIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "star24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/SendStarsStarSliderIcon.imageset/star24.pdf b/submodules/TelegramUI/Images.xcassets/Premium/SendStarsStarSliderIcon.imageset/star24.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4c22fe1f7969e50d18e4263b4744c11f3560f7f4 GIT binary patch literal 1420 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f-!Zrd)oJtTYldyoz~7} z{X-&Z)pz^p>%U#mGx+#LdAq)^`OMgD(IpzcLVhbOy;k+^c5<}exzH5{gR~!=oRK7WH1mn~4pVb?tx%=o8}D!BJ>&f4roh*`ZxyD9y>aq- zZ^*Oh^6_~oJMJ@Cc`YrfK4A9pLEkDz|Eib9hDopdd;{7qM@TJwF5$+sFeSg*$m)?@ zQ*Fuo!~3Qvv(447n%fz9M1#A&(M9IBrPS&a@u`e*>!xe!PJgAw%6#nJ6o+CzvE%Hk z9BLVs%G%4=23zcU@cOE!-QTdBG8OKckbaHKy`d2=>6VnJ6HCsj=W>U3os^B(zBT#x>pZ{7RiBpD)Sq;jc$vkm>)f7KiBo;f z9C;OLmi}akA9vh88@(U9cM8iUFZJS8f7PL7Z1AFQ^UiqAP10*;T$5RS&N$RSO>x8P ztkZXs?<~knNxAUB#)miJW380qh0RqpM|s`)?=*TQzvh{L=0n4^SP{2uSx2YhezkV4 z`?D_PDFhu0Hokj1cL~pm0#WUvd95qC?EC~8(zBfWkM7ZEST<=*r_k0Mo6}b}%AQwi zE>(MVL4YStylG$YpEiB3iqdl3rW#uz&vg z@}M)<&xC>(rpXGkfA}vjI`j5t++KxEKh8$~-0^SMzdsMF>i?Gg<*%Kp$O6ha(4-H` zL!e9n$y?^8M(~_v0v845IDN;wy!?`4h3HslehR59NLA1eNKA)iFyGXa%tWXB3WaC| z13d#UKroC&v9%HS};fW#u8n1U%( z2&otV3Bij8eecW^pi>nI-bFeL|_5HGa^|C>;|*>VX!(w9e6L8$3emY zNxj`yEvYLA5dqBF3B{n`>^pVV6um=zqbBhTOY@(jw}qm_q<)>V?vkQX<3Mp&mvGR_ z2md8G^!SNNT+V~KxS@{rH3_R}V`Du&BlQW9i&Qd;SH-s-xT8ZaMs~b!SKKOp^2{~x zvmJG?I%lwE9ACb=ocFxyzEhW7i!~{D5YK0N3|vSr*(;&s7l!!vDC(o%ddptey}Kq> zw1~v7nBpZ0(rMIQt^A^TxCD@l&>=v&xKfzO?mPQ)lgh(Nd*~yi>!7MZN>i6SZq$OlOseA zjfYBnkG7N?_!yU}b4J=ClxGxDt*dj_TCE#RV-;V!OAdv9yDo8Xu`b&Xnp=EO{PJqG z_NZ1_qU`3eeJ}4Eqe&c)2HBHxE3GD+@m8O@E=y3~w(ebX`MNfpsLllKq^ej0iLO(P zyd$Y%*k(AX%v0GmRO;7%%SiugqN*FtIBDO-%>!TUIOPopD)qvOG|pX z@>%0SONv32$!=zJoU3!k!@xpRss6p2n?a71B`tA1=noH>XJ2i1=cW66rjzHV2qQXE zNH*VI25wSP_xyCr6l=pgETk{&_^V^b<#=k(nJwM{ug!Lt+^>Y9F>WL6E)ffl7zQUc zXqn>-&pr+?tk115_9_n-ru9DA%V@9FN-(>QB21es?=yRg&ohU3Zy7U=7F?HSo^ zm<6?_|47YRjz}($4KXL{25b#dvFM6(dLwi=QDz!n!Sdq(GrZo3)RV~KET&wA zv;M^1`eF0t3h7|7-1N*!g)-%^JP^Xxnw?uFs-2f(x}ep~HiT6bv&$JpMR#(%ejRFW zo@crbqpyvQK;6k&kho<7(_7y4=%{|jAp?^U%i(!xRw=XGoQ|<_IZOT0I8~^*#6m3r zv?<^5(Gf4!Cky1s%)5Z8Y6(WLj>ReP1@Hy0hK}_QGi;W z3L*hatN?FDBB}e-#Ee9=cU1fNULp}3|0|Il)dTx}7lraQ5fQMsk+1;L{Jr&V>T%c# w`8#18u;qr~CWnIe Void { + guard let message = params.message, let contentNode = params.contentNode else { + return + } + + guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) else { + return + } + + var updatedMessages = messages + for i in 0 ..< updatedMessages.count { + if updatedMessages[i].id == message.id { + let message = updatedMessages.remove(at: i) + updatedMessages.insert(message, at: 0) + break + } + } + + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer + let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + + let source: ContextContentSource +// if let location = location { +// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) +// } else { + source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) +// } + + params.progress?.set(.single(true)) + + let _ = (self.context.engine.payments.getBankCardInfo(cardNumber: number) + |> deliverOnMainQueue).start(next: { [weak self] info in + guard let self else { + return + } + params.progress?.set(.single(false)) + + var items: [ContextMenuItem] = [] + + items.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Context_Card_Copy, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + UIPasteboard.general.string = number + + self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_CardNumberCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + })) + ) + + if let info { + items.append(.separator) + let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil + items.append(.action(ContextMenuActionItem(text: info.title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) + } + + self.canReadHistory.set(false) + + let controller = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture, disableScreenshots: false) + controller.dismissed = { [weak self] in + self?.canReadHistory.set(true) + } + + self.window?.presentInGlobalOverlay(controller) + }) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenCommandContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenCommandContextMenu.swift new file mode 100644 index 00000000000..ce1746705c9 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenCommandContextMenu.swift @@ -0,0 +1,69 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import ContextUI +import UndoUI +import AccountContext +import ChatMessageItemView +import ChatMessageItemCommon +import ChatControllerInteraction + +extension ChatControllerImpl { + func openCommandContextMenu(command: String, params: ChatControllerInteraction.LongTapParams) -> Void { + guard let message = params.message, let contentNode = params.contentNode else { + return + } + + guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) else { + return + } + + var updatedMessages = messages + for i in 0 ..< updatedMessages.count { + if updatedMessages[i].id == message.id { + let message = updatedMessages.remove(at: i) + updatedMessages.insert(message, at: 0) + break + } + } + + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer + let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + + let source: ContextContentSource +// if let location = location { +// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) +// } else { + source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) +// } + + var items: [ContextMenuItem] = [] + + items.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Context_Command_Copy, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + UIPasteboard.general.string = command + + self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + })) + ) + + self.canReadHistory.set(false) + + let controller = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture, disableScreenshots: false) + controller.dismissed = { [weak self] in + self?.canReadHistory.set(true) + } + + self.window?.presentInGlobalOverlay(controller) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenHashtagContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenHashtagContextMenu.swift new file mode 100644 index 00000000000..5a9bfd220a8 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenHashtagContextMenu.swift @@ -0,0 +1,79 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import ContextUI +import UndoUI +import AccountContext +import ChatMessageItemView +import ChatMessageItemCommon +import ChatControllerInteraction + +extension ChatControllerImpl { + func openHashtagContextMenu(hashtag: String, params: ChatControllerInteraction.LongTapParams) -> Void { + guard let message = params.message, let contentNode = params.contentNode else { + return + } + + guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) else { + return + } + + var updatedMessages = messages + for i in 0 ..< updatedMessages.count { + if updatedMessages[i].id == message.id { + let message = updatedMessages.remove(at: i) + updatedMessages.insert(message, at: 0) + break + } + } + + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer + let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + + let source: ContextContentSource +// if let location = location { +// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) +// } else { + source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) +// } + + var items: [ContextMenuItem] = [] + + items.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Context_Hashtag_Search, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Search"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + guard let self else { + return + } + f(.default) + self.controllerInteraction?.openHashtag(nil, hashtag) + })) + ) + + items.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Context_Hashtag_Copy, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + UIPasteboard.general.string = hashtag + + self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_HashtagCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + })) + ) + + self.canReadHistory.set(false) + + let controller = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture, disableScreenshots: false) + controller.dismissed = { [weak self] in + self?.canReadHistory.set(true) + } + + self.window?.presentInGlobalOverlay(controller) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift new file mode 100644 index 00000000000..c3d6c7070ba --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift @@ -0,0 +1,205 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import ContextUI +import UndoUI +import AccountContext +import ChatMessageItemView +import ChatMessageItemCommon +import MessageUI +import ChatControllerInteraction +import UrlWhitelist +import OpenInExternalAppUI +import SafariServices + +extension ChatControllerImpl { + func openLinkContextMenu(url: String, params: ChatControllerInteraction.LongTapParams) -> Void { + guard let message = params.message, let contentNode = params.contentNode else { + var (cleanUrl, _) = parseUrl(url: url, wasConcealed: false) + var canAddToReadingList = true + var canOpenIn = availableOpenInOptions(context: self.context, item: .url(url: url)).count > 1 + let mailtoString = "mailto:" + let telString = "tel:" + var openText = self.presentationData.strings.Conversation_LinkDialogOpen + var phoneNumber: String? + + var isPhoneNumber = false + var isEmail = false + var hasOpenAction = true + + if cleanUrl.hasPrefix(mailtoString) { + canAddToReadingList = false + cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...]) + isEmail = true + } else if cleanUrl.hasPrefix(telString) { + canAddToReadingList = false + phoneNumber = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...]) + cleanUrl = phoneNumber! + openText = self.presentationData.strings.UserInfo_PhoneCall + canOpenIn = false + isPhoneNumber = true + + if cleanUrl.hasPrefix("+888") { + hasOpenAction = false + } + } else if canOpenIn { + openText = self.presentationData.strings.Conversation_FileOpenIn + } + + let actionSheet = ActionSheetController(presentationData: self.presentationData) + var items: [ActionSheetItem] = [] + items.append(ActionSheetTextItem(title: cleanUrl)) + if hasOpenAction { + items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + if canOpenIn { + strongSelf.openUrlIn(url) + } else { + strongSelf.openUrl(url, concealed: false) + } + } + })) + } + if let phoneNumber = phoneNumber { + items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_AddContact, color: .accent, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.controllerInteraction?.addContact(phoneNumber) + } + })) + } + items.append(ActionSheetButtonItem(title: canAddToReadingList ? self.presentationData.strings.ShareMenu_CopyShareLink : self.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet, weak self] in + actionSheet?.dismissAnimated() + guard let self else { + return + } + UIPasteboard.general.string = cleanUrl + + let content: UndoOverlayContent + if isPhoneNumber { + content = .copy(text: self.presentationData.strings.Conversation_PhoneCopied) + } else if isEmail { + content = .copy(text: self.presentationData.strings.Conversation_EmailCopied) + } else if canAddToReadingList { + content = .linkCopied(text: self.presentationData.strings.Conversation_LinkCopied) + } else { + content = .copy(text: self.presentationData.strings.Conversation_TextCopied) + } + self.present(UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + })) + if canAddToReadingList { + items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let link = URL(string: url) { + let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) + } + })) + } + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + self.chatDisplayNode.dismissInput() + self.present(actionSheet, in: .window(.root)) + + return + } + + guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) else { + return + } + + var updatedMessages = messages + for i in 0 ..< updatedMessages.count { + if updatedMessages[i].id == message.id { + let message = updatedMessages.remove(at: i) + updatedMessages.insert(message, at: 0) + break + } + } + + var (cleanUrl, _) = parseUrl(url: url, wasConcealed: false) + var canAddToReadingList = true + let canOpenIn = availableOpenInOptions(context: self.context, item: .url(url: url)).count > 1 + + let mailtoString = "mailto:" + var openText = self.presentationData.strings.Conversation_LinkDialogOpen + + if cleanUrl.hasPrefix(mailtoString) { + canAddToReadingList = false + cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...]) +// isEmail = true + } else if canOpenIn { + openText = self.presentationData.strings.Conversation_FileOpenIn + } + + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer + let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + + let source: ContextContentSource +// if let location = location { +// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) +// } else { + source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) +// } + + var items: [ContextMenuItem] = [] + + items.append( + .action(ContextMenuActionItem(text: openText, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + guard let self else { + return + } + f(.default) + + if canOpenIn { + self.openUrlIn(url) + } + else { + self.openUrl(url, concealed: false) + } + })) + ) + + items.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuCopyLink, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + UIPasteboard.general.string = url + + self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + })) + ) + + if canAddToReadingList { + items.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_AddToReadingList, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + if let link = URL(string: url) { + let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) + } + })) + ) + } + + self.canReadHistory.set(false) + + let controller = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture, disableScreenshots: false) + controller.dismissed = { [weak self] in + self?.canReadHistory.set(true) + } + + self.window?.presentInGlobalOverlay(controller) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkLongTap.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkLongTap.swift new file mode 100644 index 00000000000..4f481295097 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkLongTap.swift @@ -0,0 +1,40 @@ +import Foundation +import Display +import ChatControllerInteraction +import AccountContext + +extension ChatControllerImpl { + func openLinkLongTap(_ action: ChatControllerInteractionLongTapAction, params: ChatControllerInteraction.LongTapParams?) { + if self.presentationInterfaceState.interfaceState.selectionState != nil { + return + } + + self.dismissAllTooltips() + + (self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() + self.chatDisplayNode.cancelInteractiveKeyboardGestures() + self.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + + guard let params else { + return + } + switch action { + case let .url(url): + self.openLinkContextMenu(url: url, params: params) + case let .mention(mention): + self.openMentionContextMenu(username: mention, peerId: nil, params: params) + case let .peerMention(peerId, mention): + self.openMentionContextMenu(username: mention, peerId: peerId, params: params) + case let .command(command): + self.openCommandContextMenu(command: command, params: params) + case let .hashtag(hashtag): + self.openHashtagContextMenu(hashtag: hashtag, params: params) + case let .timecode(value, timecode): + self.openTimecodeContextMenu(timecode: timecode, value: value, params: params) + case let .bankCard(number): + self.openBankCardContextMenu(number: number, params: params) + case let .phone(number): + self.openPhoneContextMenu(number: number, params: params) + } + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index 7b01d7f4d48..705a9d3555a 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -71,6 +71,13 @@ extension ChatControllerImpl { case .custom, .twoLists: break } + + var allowedReactions = allowedReactions + if allowedReactions != nil, case let .customChatContents(customChatContents) = self.presentationInterfaceState.subject { + if case let .hashTagSearch(publicPosts) = customChatContents.kind, publicPosts { + allowedReactions = nil + } + } var tip: ContextController.Tip? diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPhoneContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPhoneContextMenu.swift index b332e0a4d09..25067071798 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPhoneContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPhoneContextMenu.swift @@ -14,38 +14,45 @@ import AvatarNode import UndoUI import MessageUI import PeerInfoUI +import ChatControllerInteraction extension ChatControllerImpl: MFMessageComposeViewControllerDelegate { - func openPhoneContextMenu(number: String, peer: EnginePeer?, message: Message, contentNode: ContextExtractedContentContainingNode, messageNode: ASDisplayNode, frame: CGRect, anyRecognizer: UIGestureRecognizer?, location: CGPoint?) -> Void { - if self.presentationInterfaceState.interfaceState.selectionState != nil { + func openPhoneContextMenu(number: String, params: ChatControllerInteraction.LongTapParams) -> Void { + guard let message = params.message, let contentNode = params.contentNode else { return } - - self.dismissAllTooltips() - let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer - let gesture: ContextGesture? = anyRecognizer as? ContextGesture + guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) else { + return + } - if let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) { - (self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() - self.chatDisplayNode.cancelInteractiveKeyboardGestures() - var updatedMessages = messages - for i in 0 ..< updatedMessages.count { - if updatedMessages[i].id == message.id { - let message = updatedMessages.remove(at: i) - updatedMessages.insert(message, at: 0) - break - } + var updatedMessages = messages + for i in 0 ..< updatedMessages.count { + if updatedMessages[i].id == message.id { + let message = updatedMessages.remove(at: i) + updatedMessages.insert(message, at: 0) + break } - - self.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() - - let source: ContextContentSource - if let location = location { - source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) - } else { - source = .extracted(ChatMessagePhoneContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) + } + + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer + let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + + let source: ContextContentSource +// if let location = location { +// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) +// } else { + source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) +// } + + params.progress?.set(.single(true)) + + let _ = (self.context.engine.peers.resolvePeerByPhone(phone: number) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self else { + return } + params.progress?.set(.single(false)) let phoneNumber: String if let peer, case let .user(user) = peer, let phone = user.phone { @@ -182,7 +189,7 @@ extension ChatControllerImpl: MFMessageComposeViewControllerDelegate { } self.window?.presentInGlobalOverlay(controller) - } + }) } private func inviteToTelegram(numbers: [String]) { @@ -207,7 +214,7 @@ extension ChatControllerImpl: MFMessageComposeViewControllerDelegate { } } -private final class ChatMessagePhoneContextExtractedContentSource: ContextExtractedContentSource { +final class ChatMessageLinkContextExtractedContentSource: ContextExtractedContentSource { let keepInPlace: Bool = false let ignoreContentTouches: Bool = true let blurBackground: Bool = true diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTimecodeContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTimecodeContextMenu.swift new file mode 100644 index 00000000000..0bc97815adb --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTimecodeContextMenu.swift @@ -0,0 +1,69 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import ContextUI +import UndoUI +import AccountContext +import ChatMessageItemView +import ChatMessageItemCommon +import ChatControllerInteraction + +extension ChatControllerImpl { + func openTimecodeContextMenu(timecode: String, value: Double, params: ChatControllerInteraction.LongTapParams) -> Void { + guard let message = params.message, let contentNode = params.contentNode else { + return + } + + guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) else { + return + } + + var updatedMessages = messages + for i in 0 ..< updatedMessages.count { + if updatedMessages[i].id == message.id { + let message = updatedMessages.remove(at: i) + updatedMessages.insert(message, at: 0) + break + } + } + + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer + let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + + let source: ContextContentSource +// if let location = location { +// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) +// } else { + source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) +// } + + var items: [ContextMenuItem] = [] + + items.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Context_Timecode_Copy, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + UIPasteboard.general.string = timecode + + self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + })) + ) + + self.canReadHistory.set(false) + + let controller = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture, disableScreenshots: false) + controller.dismissed = { [weak self] in + self?.canReadHistory.set(true) + } + + self.window?.presentInGlobalOverlay(controller) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenUsernameContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenUsernameContextMenu.swift new file mode 100644 index 00000000000..ae629872f8e --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenUsernameContextMenu.swift @@ -0,0 +1,161 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import ContextUI +import UndoUI +import AccountContext +import ChatMessageItemView +import ChatMessageItemCommon +import AvatarNode +import ChatControllerInteraction + +extension ChatControllerImpl { + func openMentionContextMenu(username: String, peerId: EnginePeer.Id?, params: ChatControllerInteraction.LongTapParams) -> Void { + guard let message = params.message, let contentNode = params.contentNode else { + return + } + + guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) else { + return + } + + var updatedMessages = messages + for i in 0 ..< updatedMessages.count { + if updatedMessages[i].id == message.id { + let message = updatedMessages.remove(at: i) + updatedMessages.insert(message, at: 0) + break + } + } + + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer + let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + + let source: ContextContentSource +// if let location = location { +// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) +// } else { + source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) +// } + + params.progress?.set(.single(true)) + + let peer: Signal + if let peerId { + peer = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + } else { + peer = self.context.engine.peers.resolvePeerByName(name: username) + |> mapToSignal { value in + switch value { + case .progress: + return .complete() + case let .result(result): + return .single(result) + } + } + } + + let _ = (peer + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self else { + return + } + params.progress?.set(.single(false)) + + var items: [ContextMenuItem] = [] + if let peer { + if case .user = peer { + items.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Context_Username_SendMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MessageBubble"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + guard let self else { + return + } + self.openPeer(peer: peer, navigation: .chat(textInputState: nil, subject: nil, peekData: nil), fromMessage: nil) + })) + ) + } else { + var isGroup = true + if case let .channel(channel) = peer, case .broadcast = channel.info { + isGroup = false + } + + let openTitle: String + let openIcon: UIImage? + + if isGroup { + openTitle = self.presentationData.strings.Chat_Context_Username_OpenGroup + openIcon = UIImage(bundleImageName: "Chat/Context Menu/Groups") + } else { + openTitle = self.presentationData.strings.Chat_Context_Username_OpenChannel + openIcon = UIImage(bundleImageName: "Chat/Context Menu/Channels") + } + items.append( + .action(ContextMenuActionItem(text: openTitle, icon: { theme in return generateTintedImage(image: openIcon, color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + guard let self else { + return + } + self.openPeer(peer: peer, navigation: .chat(textInputState: nil, subject: nil, peekData: nil), fromMessage: nil) + })) + ) + } + } + + items.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Context_Username_Copy, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + UIPasteboard.general.string = username + + self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_UsernameCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + })) + ) + + items.append(.separator) + if let peer { + let avatarSize = CGSize(width: 28.0, height: 28.0) + let avatarSignal = peerAvatarCompleteImage(account: self.context.account, peer: peer, size: avatarSize) + + let subtitle = NSMutableAttributedString(string: self.presentationData.strings.Chat_Context_Phone_ViewProfile + " >") + if let range = subtitle.string.range(of: ">"), let arrowImage = UIImage(bundleImageName: "Item List/InlineTextRightArrow") { + subtitle.addAttribute(.attachment, value: arrowImage, range: NSRange(range, in: subtitle.string)) + subtitle.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: subtitle.string)) + } + + items.append( + .action(ContextMenuActionItem(text: peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), textLayout: .secondLineWithAttributedValue(subtitle), icon: { theme in return nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), iconPosition: .left, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + self.openPeer(peer: peer, navigation: .info(ChatControllerInteractionNavigateToPeer.InfoParams(ignoreInSavedMessages: true)), fromMessage: nil) + })) + ) + } else { + let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil + items.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Context_Username_NotOnTelegram, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction)) + ) + } + + self.canReadHistory.set(false) + + let controller = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture, disableScreenshots: false) + controller.dismissed = { [weak self] in + self?.canReadHistory.set(true) + } + + self.window?.presentInGlobalOverlay(controller) + }) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift new file mode 100644 index 00000000000..579cc0fe4f2 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -0,0 +1,412 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import TelegramCore +import ChatPresentationInterfaceState +import ChatControllerInteraction +import WebUI +import AttachmentUI +import AccountContext +import TelegramNotices +import PresentationDataUtils +import UndoUI +import UrlHandling + +public extension ChatControllerImpl { + func openWebApp(buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource) { + guard let peerId = self.chatLocation.peerId, let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return + } + self.chatDisplayNode.dismissInput() + + let botName: String + let botAddress: String + if case let .inline(bot) = source { + botName = bot.compactDisplayTitle + botAddress = bot.addressName ?? "" + } else { + botName = EnginePeer(peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) + botAddress = peer.addressName ?? "" + } + + if source == .generic { + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if !$0.contains(where: { + switch $0 { + case .requestInProgress: + return true + default: + return false + } + }) { + var updatedContexts = $0 + updatedContexts.append(.requestInProgress) + return updatedContexts.sorted() + } + return $0 + } + }) + } + + let updateProgress = { [weak self] in + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if let index = $0.firstIndex(where: { + switch $0 { + case .requestInProgress: + return true + default: + return false + } + }) { + var updatedContexts = $0 + updatedContexts.remove(at: index) + return updatedContexts + } + return $0 + } + }) + } + } + } + + let openWebView = { + if source == .menu { + self.updateChatPresentationInterfaceState(interactive: false) { state in + return state.updatedForceInputCommandsHidden(true) +// return state.updatedShowWebView(true).updatedForceInputCommandsHidden(true) + } + + if let navigationController = self.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer { + for controller in minimizedContainer.controllers { + if let controller = controller as? AttachmentController, let mainController = controller.mainController as? WebAppController, mainController.botId == peerId && mainController.source == .menu { + navigationController.maximizeViewController(controller, animated: true) + return + } + } + } + + var fullSize = false + if isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: self.context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl { + fullSize = !url.contains("?mode=compact") + } + + let context = self.context + let params = WebAppParameters(source: .menu, peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: fullSize) + let controller = standaloneWebAppController(context: self.context, updatedPresentationData: self.updatedPresentationData, params: params, threadId: self.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in + self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) + }, requestSwitchInline: { [weak self] query, chatTypes, completion in + if let strongSelf = self { + if let chatTypes { + let controller = strongSelf.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: strongSelf.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: chatTypes, hasContactSelector: false, hasCreation: false)) + controller.peerSelected = { [weak self, weak controller] peer, _ in + if let strongSelf = self { + completion() + controller?.dismiss() + strongSelf.controllerInteraction?.activateSwitchInline(peer.id, "@\(botAddress) \(query)", nil) + } + } + strongSelf.push(controller) + } else { + strongSelf.controllerInteraction?.activateSwitchInline(peerId, "@\(botAddress) \(query)", nil) + } + } + }, getInputContainerNode: { [weak self] in + if let strongSelf = self, let layout = strongSelf.validLayout, case .compact = layout.metrics.widthClass { + return (strongSelf.chatDisplayNode.getWindowInputAccessoryHeight(), strongSelf.chatDisplayNode.inputPanelContainerNode, { + return strongSelf.chatDisplayNode.textInputPanelNode?.makeAttachmentMenuTransition(accessoryPanelNode: nil) + }) + } else { + return nil + } + }, completion: { [weak self] in + self?.chatDisplayNode.historyNode.scrollToEndOfHistory() + }, willDismiss: { [weak self] in + self?.interfaceInteraction?.updateShowWebView { _ in + return false + } + }, didDismiss: { [weak self] in + if let strongSelf = self { + let isFocused = strongSelf.chatDisplayNode.textInputPanelNode?.isFocused ?? false + strongSelf.chatDisplayNode.insertSubnode(strongSelf.chatDisplayNode.inputPanelContainerNode, aboveSubnode: strongSelf.chatDisplayNode.inputContextPanelContainer) + if isFocused { + strongSelf.chatDisplayNode.textInputPanelNode?.ensureFocused() + } + + strongSelf.updateChatPresentationInterfaceState(interactive: false) { state in + return state.updatedForceInputCommandsHidden(false) + } + } + }, getNavigationController: { [weak self] in + return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController + }) + controller.navigationPresentation = .flatModal + self.push(controller) + } else if simple { + var isInline = false + var botId = peerId + var botName = botName + var botAddress = "" + if case let .inline(bot) = source { + isInline = true + botId = bot.id + botName = bot.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) + botAddress = bot.addressName ?? "" + } + + self.messageActionCallbackDisposable.set(((self.context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(self.presentationData.theme)) + |> afterDisposed { + updateProgress() + }) + |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let strongSelf = self else { + return + } + let context = strongSelf.context + let params = WebAppParameters(source: isInline ? .inline : .simple, peerId: peerId, botId: botId, botName: botName, url: result.url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) + let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in + self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) + }, requestSwitchInline: { [weak self] query, chatTypes, completion in + if let strongSelf = self { + if let chatTypes { + let controller = strongSelf.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: strongSelf.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: chatTypes, hasContactSelector: false, hasCreation: false)) + controller.peerSelected = { [weak self, weak controller] peer, _ in + if let strongSelf = self { + completion() + controller?.dismiss() + strongSelf.controllerInteraction?.activateSwitchInline(peer.id, "@\(botAddress) \(query)", nil) + } + } + strongSelf.push(controller) + } else { + strongSelf.controllerInteraction?.activateSwitchInline(peerId, "@\(botAddress) \(query)", nil) + } + } + }, getNavigationController: { [weak self] in + return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController + }) + controller.navigationPresentation = .flatModal + strongSelf.currentWebAppController = controller + strongSelf.push(controller) + }, error: { [weak self] error in + if let strongSelf = self { + strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) + } + })) + } else { + self.messageActionCallbackDisposable.set(((self.context.engine.messages.requestWebView(peerId: peerId, botId: peerId, url: !url.isEmpty ? url : nil, payload: nil, themeParams: generateWebAppThemeParams(self.presentationData.theme), fromMenu: buttonText == "Menu", replyToMessageId: nil, threadId: self.chatLocation.threadId) + |> afterDisposed { + updateProgress() + }) + |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let strongSelf = self else { + return + } + let context = strongSelf.context + let params = WebAppParameters(source: .generic, peerId: peerId, botId: peerId, botName: botName, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) + let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in + self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) + }, completion: { [weak self] in + self?.chatDisplayNode.historyNode.scrollToEndOfHistory() + }, getNavigationController: { [weak self] in + return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController + }) + controller.navigationPresentation = .flatModal + strongSelf.currentWebAppController = controller + strongSelf.push(controller) + }, error: { [weak self] error in + if let strongSelf = self { + strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) + } + })) + } + } + + var botPeer = EnginePeer(peer) + if case let .inline(bot) = source { + botPeer = bot + } + let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: self.context.sharedContext.accountManager, peerId: botPeer.id) + |> deliverOnMainQueue).startStandalone(next: { [weak self] value in + guard let strongSelf = self else { + return + } + + if value { + openWebView() + } else { + let controller = webAppLaunchConfirmationController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: botPeer, completion: { _ in + let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() + openWebView() + }, showMore: nil) + strongSelf.present(controller, in: .window(.root)) + } + }) + } + + func presentBotApp(botApp: BotApp, botPeer: EnginePeer, payload: String?, compact: Bool, concealed: Bool = false, commit: @escaping () -> Void = {}) { + guard let peerId = self.chatLocation.peerId else { + return + } + self.attachmentController?.dismiss(animated: true, completion: nil) + + let openBotApp: (Bool, Bool) -> Void = { [weak self] allowWrite, justInstalled in + guard let strongSelf = self else { + return + } + commit() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if !$0.contains(where: { + switch $0 { + case .requestInProgress: + return true + default: + return false + } + }) { + var updatedContexts = $0 + updatedContexts.append(.requestInProgress) + return updatedContexts.sorted() + } + return $0 + } + }) + + let updateProgress = { [weak self] in + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if let index = $0.firstIndex(where: { + switch $0 { + case .requestInProgress: + return true + default: + return false + } + }) { + var updatedContexts = $0 + updatedContexts.remove(at: index) + return updatedContexts + } + return $0 + } + }) + } + } + } + + let botAddress = botPeer.addressName ?? "" + strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestAppWebView(peerId: peerId, appReference: .id(id: botApp.id, accessHash: botApp.accessHash), payload: payload, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme), compact: compact, allowWrite: allowWrite) + |> afterDisposed { + updateProgress() + }) + |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let strongSelf = self else { + return + } + let context = strongSelf.context + let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize)) + let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in + self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) + }, requestSwitchInline: { [weak self] query, chatTypes, completion in + if let strongSelf = self { + if let chatTypes { + let controller = strongSelf.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: strongSelf.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: chatTypes, hasContactSelector: false, hasCreation: false)) + controller.peerSelected = { [weak self, weak controller] peer, _ in + if let strongSelf = self { + completion() + controller?.dismiss() + strongSelf.controllerInteraction?.activateSwitchInline(peer.id, "@\(botAddress) \(query)", nil) + } + } + strongSelf.push(controller) + } else { + strongSelf.controllerInteraction?.activateSwitchInline(peerId, "@\(botAddress) \(query)", nil) + } + } + }, completion: { [weak self] in + self?.chatDisplayNode.historyNode.scrollToEndOfHistory() + }, getNavigationController: { [weak self] in + return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController + }) + controller.navigationPresentation = .flatModal + strongSelf.currentWebAppController = controller + strongSelf.push(controller) + + if justInstalled { + let content: UndoOverlayContent = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(botPeer.compactDisplayTitle).string, timeout: 5.0, customUndoText: nil) + controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current) + } + }, error: { [weak self] error in + if let strongSelf = self { + strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) + } + })) + } + + let _ = combineLatest( + queue: Queue.mainQueue(), + ApplicationSpecificNotice.getBotGameNotice(accountManager: self.context.sharedContext.accountManager, peerId: botPeer.id), + self.context.engine.messages.attachMenuBots(), + self.context.engine.messages.getAttachMenuBot(botId: botPeer.id, cached: true) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + ).startStandalone(next: { [weak self] noticed, attachMenuBots, attachMenuBot in + guard let self else { + return + } + + var isAttachMenuBotInstalled: Bool? + if let _ = attachMenuBot { + if let _ = attachMenuBots.first(where: { $0.peer.id == botPeer.id && !$0.flags.contains(.notActivated) }) { + isAttachMenuBotInstalled = true + } else { + isAttachMenuBotInstalled = false + } + } + + let context = self.context + if !noticed || botApp.flags.contains(.notActivated) || isAttachMenuBotInstalled == false { + if let isAttachMenuBotInstalled, let attachMenuBot { + if !isAttachMenuBotInstalled { + let controller = webAppTermsAlertController(context: context, updatedPresentationData: self.updatedPresentationData, bot: attachMenuBot, completion: { allowWrite in + let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() + let _ = (context.engine.messages.addBotToAttachMenu(botId: botPeer.id, allowWrite: allowWrite) + |> deliverOnMainQueue).startStandalone(error: { _ in + }, completed: { + openBotApp(allowWrite, true) + }) + }) + self.present(controller, in: .window(.root)) + } else { + openBotApp(false, false) + } + } else { + let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: self.updatedPresentationData, peer: botPeer, requestWriteAccess: botApp.flags.contains(.notActivated) && botApp.flags.contains(.requiresWriteAccess), completion: { allowWrite in + let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() + openBotApp(allowWrite, false) + }, showMore: { [weak self] in + if let self { + self.openResolved(result: .peer(botPeer._asPeer(), .info(nil)), sourceMessageId: nil) + } + }) + self.present(controller, in: .window(.root)) + } + } else { + openBotApp(false, false) + } + }) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift index 937c739c156..88a9c0d8aa9 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift @@ -49,7 +49,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } }) } }, nil) @@ -67,7 +67,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } }) } }, nil) @@ -97,7 +97,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } }) } }, nil) @@ -115,7 +115,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } }) } }, nil) diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift index 179f566e64a..0951822b759 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -19,6 +19,7 @@ import TelegramNotices import ChatMessageWebpageBubbleContentNode import PremiumUI import UndoUI +import WebsiteType private enum OptionsId: Hashable { case reply @@ -102,7 +103,7 @@ private func chatForwardOptions(selfController: ChatControllerImpl, sourceNode: } |> distinctUntilChanged - let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [peerId], ids: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .forward(ChatControllerSubject.MessageOptionsInfo.Forward(options: forwardOptions))), botStart: nil, mode: .standard(.previewing)) + let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [peerId], ids: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .forward(ChatControllerSubject.MessageOptionsInfo.Forward(options: forwardOptions))), botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) let messageIds = selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] @@ -133,6 +134,7 @@ private func chatForwardOptions(selfController: ChatControllerImpl, sourceNode: var hasOther = false var hasNotOwnMessages = false + var hasPaid = false for message in messages { if let author = message.effectiveAuthor { if !uniquePeerIds.contains(author.id) { @@ -158,6 +160,8 @@ private func chatForwardOptions(selfController: ChatControllerImpl, sourceNode: if !message.text.isEmpty { hasCaptions = true } + } else if media is TelegramMediaPaidContent { + hasPaid = true } } if !isDice && !isMusic { @@ -169,6 +173,9 @@ private func chatForwardOptions(selfController: ChatControllerImpl, sourceNode: if case let .peer(peerId) = selfController.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { canHideNames = false } + if hasPaid { + canHideNames = false + } let hideNames = forwardOptions.hideNames let hideCaptions = forwardOptions.hideCaptions @@ -193,7 +200,7 @@ private func chatForwardOptions(selfController: ChatControllerImpl, sourceNode: }))) } - if hasCaptions { + if hasCaptions && !hasPaid { items.append(.action(ContextMenuActionItem(text: hideCaptions ? presentationData.strings.Conversation_ForwardOptions_ShowCaption : presentationData.strings.Conversation_ForwardOptions_HideCaption, icon: { _ in return nil }, iconAnimation: ContextMenuActionItem.IconAnimation( @@ -471,7 +478,7 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch } var replySubject = replySubject replySubject.quote = nil - selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil).withoutSelectionState() }).updatedSearch(nil) }) + selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withoutSelectionState() }).updatedSearch(nil) }) }))) } @@ -513,7 +520,7 @@ private func chatReplyOptions(selfController: ChatControllerImpl, sourceNode: AS } |> distinctUntilChanged) - guard let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [replySubject.messageId.peerId], ids: [replySubject.messageId], info: .reply(ChatControllerSubject.MessageOptionsInfo.Reply(quote: replyQuote, selectionState: selectionState))), botStart: nil, mode: .standard(.previewing)) as? ChatControllerImpl else { + guard let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [replySubject.messageId.peerId], ids: [replySubject.messageId], info: .reply(ChatControllerSubject.MessageOptionsInfo.Reply(quote: replyQuote, selectionState: selectionState))), botStart: nil, mode: .standard(.previewing), params: nil) as? ChatControllerImpl else { return nil } chatController.canReadHistory.set(false) @@ -616,78 +623,93 @@ func moveReplyMessageToAnotherChat(selfController: ChatControllerImpl, replySubj selfController.searchResultsController = nil strongController.dismiss() } else { - if let navigationController = selfController.navigationController as? NavigationController { - for controller in navigationController.viewControllers { - if let maybeChat = controller as? ChatControllerImpl { - if case .peer(peerId) = maybeChat.chatLocation { - var isChatPinnedMessages = false - if case .pinnedMessages = maybeChat.presentationInterfaceState.subject { - isChatPinnedMessages = true - } - if !isChatPinnedMessages { - maybeChat.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }) }) - selfController.dismiss() - strongController.dismiss() - return - } - } - } - } - } - - let _ = (ChatInterfaceState.update(engine: selfController.context.engine, peerId: peerId, threadId: threadId, { currentState in - return currentState.withUpdatedReplyMessageSubject(replySubject) + moveReplyToChat(selfController: selfController, peerId: peerId, threadId: threadId, replySubject: replySubject, completion: { [weak strongController] in + strongController?.dismiss() }) - |> deliverOnMainQueue).startStandalone(completed: { [weak selfController] in - guard let selfController else { - return + } + } + selfController.chatDisplayNode.dismissInput() + selfController.effectiveNavigationController?.pushViewController(controller) + }) +} + +func moveReplyToChat(selfController: ChatControllerImpl, peerId: EnginePeer.Id, threadId: Int64?, replySubject: ChatInterfaceState.ReplyMessageSubject, completion: @escaping () -> Void) { + if let navigationController = selfController.effectiveNavigationController { + for controller in navigationController.viewControllers { + if let maybeChat = controller as? ChatControllerImpl { + if case .peer(peerId) = maybeChat.chatLocation { + var isChatPinnedMessages = false + if case .pinnedMessages = maybeChat.presentationInterfaceState.subject { + isChatPinnedMessages = true } - let proceed: (ChatController) -> Void = { [weak selfController] chatController in - guard let selfController else { - return - } - selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil).withoutSelectionState() }) }) + if !isChatPinnedMessages { + maybeChat.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }) }) - let navigationController: NavigationController? - if let parentController = selfController.parentController { - navigationController = (parentController.navigationController as? NavigationController) + var viewControllers = navigationController.viewControllers + if let index = viewControllers.firstIndex(where: { $0 === maybeChat }), index != viewControllers.count - 1 { + viewControllers.removeSubrange((index + 1) ..< viewControllers.count) + navigationController.setViewControllers(viewControllers, animated: true) } else { - navigationController = selfController.effectiveNavigationController + selfController.dismiss() } - if let navigationController = navigationController { - var viewControllers = navigationController.viewControllers - if threadId != nil { - viewControllers.insert(chatController, at: viewControllers.count - 2) - } else { - viewControllers.insert(chatController, at: viewControllers.count - 1) - } - navigationController.setViewControllers(viewControllers, animated: false) - - selfController.controllerNavigationDisposable.set((chatController.ready.get() - |> SwiftSignalKit.filter { $0 } - |> take(1) - |> deliverOnMainQueue).startStrict(next: { [weak navigationController] _ in - viewControllers.removeAll(where: { $0 is PeerSelectionController }) - navigationController?.setViewControllers(viewControllers, animated: true) - })) - } - } - if let threadId = threadId { - let _ = (selfController.context.sharedContext.chatControllerForForumThread(context: selfController.context, peerId: peerId, threadId: threadId) - |> deliverOnMainQueue).startStandalone(next: { chatController in - proceed(chatController) - }) - } else { - let chatController = ChatControllerImpl(context: selfController.context, chatLocation: .peer(id: peerId)) - chatController.activateInput(type: .text) - proceed(chatController) + completion() + return } - }) + } } } - selfController.chatDisplayNode.dismissInput() - selfController.effectiveNavigationController?.pushViewController(controller) + } + + let _ = (ChatInterfaceState.update(engine: selfController.context.engine, peerId: peerId, threadId: threadId, { currentState in + return currentState.withUpdatedReplyMessageSubject(replySubject) + }) + |> deliverOnMainQueue).startStandalone(completed: { [weak selfController] in + guard let selfController else { + return + } + let proceed: (ChatController) -> Void = { [weak selfController] chatController in + guard let selfController else { + return + } + selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withoutSelectionState() }) }) + + let navigationController: NavigationController? + if let parentController = selfController.parentController { + navigationController = (parentController.navigationController as? NavigationController) + } else { + navigationController = selfController.effectiveNavigationController + } + + if let navigationController = navigationController { + var viewControllers = navigationController.viewControllers + if threadId != nil { + viewControllers.insert(chatController, at: viewControllers.count - 2) + } else { + viewControllers.insert(chatController, at: viewControllers.count - 1) + } + navigationController.setViewControllers(viewControllers, animated: false) + + selfController.controllerNavigationDisposable.set((chatController.ready.get() + |> SwiftSignalKit.filter { $0 } + |> take(1) + |> timeout(0.2, queue: .mainQueue(), alternate: .single(true)) + |> deliverOnMainQueue).startStrict(next: { [weak navigationController] _ in + viewControllers.removeAll(where: { $0 is PeerSelectionController }) + navigationController?.setViewControllers(viewControllers, animated: true) + })) + } + } + if let threadId = threadId { + let _ = (selfController.context.sharedContext.chatControllerForForumThread(context: selfController.context, peerId: peerId, threadId: threadId) + |> deliverOnMainQueue).startStandalone(next: { chatController in + proceed(chatController) + }) + } else { + let chatController = ChatControllerImpl(context: selfController.context, chatLocation: .peer(id: peerId)) + chatController.activateInput(type: .text) + proceed(chatController) + } }) } @@ -769,7 +791,7 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD } |> distinctUntilChanged - guard let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [peerId], ids: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .link(ChatControllerSubject.MessageOptionsInfo.Link(options: linkOptions))), botStart: nil, mode: .standard(.previewing)) as? ChatControllerImpl else { + guard let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [peerId], ids: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .link(ChatControllerSubject.MessageOptionsInfo.Link(options: linkOptions, isCentered: false))), botStart: nil, mode: .standard(.previewing), params: nil) as? ChatControllerImpl else { return nil } chatController.canReadHistory.set(false) diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift index cbc77c660a4..d0380195e17 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift @@ -68,14 +68,22 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no editMessages = .single([]) } + var currentMessageEffect: ChatSendMessageActionSheetControllerSendParameters.Effect? + if selfController.presentationInterfaceState.interfaceState.editMessage == nil { + if let sendMessageEffect = selfController.presentationInterfaceState.interfaceState.sendMessageEffect { + currentMessageEffect = ChatSendMessageActionSheetControllerSendParameters.Effect(id: sendMessageEffect) + } + } + let _ = (combineLatest( selfController.context.account.viewTracker.peerView(peerId) |> take(1), effectItems, availableMessageEffects, hasPremium, - editMessages + editMessages, + ChatSendMessageContextScreen.initialData(context: selfController.context, currentMessageEffectId: currentMessageEffect?.id) ) - |> deliverOnMainQueue).startStandalone(next: { [weak selfController] peerView, effectItems, availableMessageEffects, hasPremium, editMessages in + |> deliverOnMainQueue).startStandalone(next: { [weak selfController] peerView, effectItems, availableMessageEffects, hasPremium, editMessages, initialData in guard let selfController, let peer = peerViewMainPeer(peerView) else { return } @@ -144,6 +152,8 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no return true } else if let file = media as? TelegramMediaFile, file.isVideo { return true + } else if media is TelegramMediaPaidContent { + return true } return false }) @@ -171,6 +181,7 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no // MARK: Nicegram TranslateEnteredMessage nicegramData: nicegramData, // + initialData: initialData, context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, peerId: selfController.presentationInterfaceState.chatLocation.peerId, @@ -262,6 +273,7 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no // MARK: Nicegram TranslateEnteredMessage nicegramData: nicegramData, // + initialData: initialData, context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, peerId: selfController.presentationInterfaceState.chatLocation.peerId, @@ -269,6 +281,16 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no isScheduledMessages: false, mediaPreview: mediaPreview, mediaCaptionIsAbove: nil, + messageEffect: (currentMessageEffect, { [weak selfController] updatedEffect in + guard let selfController else { + return + } + selfController.updateChatPresentationInterfaceState(transition: .immediate, interactive: true, { presentationInterfaceState in + return presentationInterfaceState.updatedInterfaceState { interfaceState in + return interfaceState.withUpdatedSendMessageEffect(updatedEffect?.id) + } + }) + }), attachment: false, canSendWhenOnline: sendWhenOnlineAvailable, forwardMessageIds: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] @@ -300,7 +322,7 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no return } selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, saveInterfaceState: selfController.presentationInterfaceState.subject != .scheduledMessages, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } }) selfController.openScheduledMessages() } diff --git a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift index 71709a4294d..62e682f00f9 100644 --- a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift +++ b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift @@ -574,15 +574,7 @@ func updateChatPresentationInterfaceStateImpl( controller.updateVisibility() } } - - if let currentMenuWebAppController = selfController.currentMenuWebAppController, !selfController.presentationInterfaceState.showWebView { - selfController.currentMenuWebAppController = nil - if let currentMenuWebAppController = currentMenuWebAppController as? AttachmentController { - currentMenuWebAppController.ensureUnfocused = false - } - currentMenuWebAppController.dismiss(animated: true, completion: nil) - } - + selfController.presentationInterfaceStatePromise.set(selfController.presentationInterfaceState) if case .tag = selfController.chatDisplayNode.historyNode.tag { diff --git a/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift index b17db6edb03..4fe8585ec15 100644 --- a/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift @@ -69,7 +69,7 @@ private final class ChatBusinessLinkTitlePanelComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ChatBusinessLinkTitlePanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChatBusinessLinkTitlePanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let size = CGSize(width: availableSize.width, height: 40.0) @@ -140,7 +140,7 @@ private final class ChatBusinessLinkTitlePanelComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -210,7 +210,7 @@ final class ChatBusinessLinkTitlePanelNode: ChatTitleAccessoryPanelNode { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) let contentSize = self.content.update( - transition: Transition(transition), + transition: ComponentTransition(transition), component: AnyComponent(ChatBusinessLinkTitlePanelComponent( context: self.context, theme: interfaceState.theme, diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 87c8ecc9bdb..e0529989c7c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -140,6 +140,8 @@ import ChatMediaInputStickerGridItem import AdsInfoScreen import MessageUI import PhoneNumberFormat +import OwnershipTransferController +import OldChannelsController public enum ChatControllerPeekActions { case standard @@ -424,6 +426,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return (self.presentationData, self.presentationDataPromise.get()) } var presentationDataDisposable: Disposable? + var forcedTheme: PresentationTheme? + var forcedNavigationBarTheme: PresentationTheme? + var forcedWallpaper: TelegramWallpaper? var automaticMediaDownloadSettings: MediaAutoDownloadSettings var automaticMediaDownloadSettingsDisposable: Disposable? @@ -534,7 +539,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let chatLocationContextHolder: Atomic weak var attachmentController: AttachmentController? - weak var currentMenuWebAppController: ViewController? weak var currentWebAppController: ViewController? weak var currentImportMessageTooltip: UndoOverlayController? @@ -634,7 +638,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, mode: ChatControllerPresentationMode = .standard(.default), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], customChatNavigationStack: [EnginePeer.Id]? = nil) { + public init( + context: AccountContext, + chatLocation: ChatLocation, + chatLocationContextHolder: Atomic = Atomic(value: nil), + subject: ChatControllerSubject? = nil, + botStart: ChatControllerInitialBotStart? = nil, + attachBotStart: ChatControllerInitialAttachBotStart? = nil, + botAppStart: ChatControllerInitialBotAppStart? = nil, + mode: ChatControllerPresentationMode = .standard(.default), + peekData: ChatPeekTimeout? = nil, + peerNearbyData: ChatPeerNearbyData? = nil, + chatListFilter: Int32? = nil, + chatNavigationStack: [ChatNavigationStackItem] = [], + customChatNavigationStack: [EnginePeer.Id]? = nil, + params: ChatControllerParams? = nil + ) { let _ = ChatControllerCount.modify { value in return value + 1 } @@ -650,6 +669,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.currentChatListFilter = chatListFilter self.chatNavigationStack = chatNavigationStack self.customChatNavigationStack = customChatNavigationStack + + self.forcedTheme = params?.forcedTheme + self.forcedNavigationBarTheme = params?.forcedNavigationBarTheme + self.forcedWallpaper = params?.forcedWallpaper var useSharedAnimationPhase = false switch mode { @@ -696,7 +719,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatLocationInfoData = .customChatContents } - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + var presentationData = context.sharedContext.currentPresentationData.with { $0 } + if let forcedTheme = self.forcedTheme { + presentationData = presentationData.withUpdated(theme: forcedTheme) + } + if let forcedWallpaper = self.forcedWallpaper { + presentationData = presentationData.withUpdated(chatWallpaper: forcedWallpaper) + } + self.presentationData = presentationData self.automaticMediaDownloadSettings = context.sharedContext.currentAutomaticMediaDownloadSettings self.stickerSettings = ChatInterfaceStickerSettings() @@ -893,11 +923,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } - if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia { + if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first { switch extendedMedia { case .preview: if displayVoiceMessageDiscardAlert() { - strongSelf.controllerInteraction?.openCheckoutOrReceipt(message.id) + strongSelf.controllerInteraction?.openCheckoutOrReceipt(message.id, params) + return true + } else { + return false + } + case .full: + break + } + } else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia { + switch extendedMedia { + case .preview: + if displayVoiceMessageDiscardAlert() { + strongSelf.controllerInteraction?.openCheckoutOrReceipt(message.id, nil) return true } else { return false @@ -1235,7 +1277,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G standalone = true } - return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, standalone: standalone, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: { + return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, mediaIndex: params.mediaIndex, standalone: standalone, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: { self?.chatDisplayNode.dismissInput() }, present: { c, a in self?.present(c, in: .window(.root), with: a, blockInteraction: true) @@ -1443,6 +1485,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case .default = reaction, strongSelf.chatLocation.peerId == strongSelf.context.account.peerId { return } + if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject { + if case let .hashTagSearch(publicPosts) = customChatContents.kind, publicPosts { + return + } + } if !force && message.areReactionsTags(accountPeerId: strongSelf.context.account.peerId) { if case .pinnedMessages = strongSelf.subject { @@ -1725,6 +1772,29 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageIds: [message.id], reactions: mappedUpdatedReactions, isLarge: false, storeAsRecentlyUsed: false).startStandalone() + + #if DEBUG + if strongSelf.context.sharedContext.applicationBindings.appBuildType == .internal { + if mappedUpdatedReactions.contains(where: { + if case let .custom(fileId, _) = $0, fileId == MessageReaction.starsReactionId { + return true + } else { + return false + } + }) { + let _ = (strongSelf.context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId]) + |> deliverOnMainQueue).start(next: { [weak strongSelf] files in + guard let strongSelf, let file = files[MessageReaction.starsReactionId] else { + return + } + //TODO:localize + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .starsSent(context: strongSelf.context, file: file, amount: 1, title: "Star Sent", text: "Long tap on {star} to select custom quantity of stars."), elevatedLayout: false, action: { _ in + return false + }), in: .current) + }) + } + } + #endif } }) }, activateMessagePinch: { [weak self] sourceNode in @@ -1816,7 +1886,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } }) } }, nil) @@ -1874,7 +1944,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var current = current current = current.updatedInterfaceState { interfaceState in var interfaceState = interfaceState - interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil) + interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) if clearInput { interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString())) } @@ -2006,7 +2076,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }.updatedInputMode { current in + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) }.updatedInputMode { current in if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { return .media(mode: mode, expanded: nil, focused: focused) } @@ -2391,7 +2461,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> deliverOnMainQueue).startStandalone(next: { coordinate in if let strongSelf = self { if let coordinate = coordinate { - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } else { strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})]), in: .window(.root)) } @@ -2450,7 +2520,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreviews([]) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreviews([]) } }) } }, nil) @@ -2563,365 +2633,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }) } - }, longTap: { [weak self] action, message in - if let strongSelf = self { - let presentationData = strongSelf.presentationData - switch action { - case let .url(url): - var (cleanUrl, _) = parseUrl(url: url, wasConcealed: false) - var canAddToReadingList = true - var canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1 - let mailtoString = "mailto:" - let telString = "tel:" - var openText = strongSelf.presentationData.strings.Conversation_LinkDialogOpen - var phoneNumber: String? - - var isPhoneNumber = false - var isEmail = false - var hasOpenAction = true - - if cleanUrl.hasPrefix(mailtoString) { - canAddToReadingList = false - cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...]) - isEmail = true - } else if cleanUrl.hasPrefix(telString) { - canAddToReadingList = false - phoneNumber = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...]) - cleanUrl = phoneNumber! - openText = strongSelf.presentationData.strings.UserInfo_PhoneCall - canOpenIn = false - isPhoneNumber = true - - if cleanUrl.hasPrefix("+888") { - hasOpenAction = false - } - } else if canOpenIn { - openText = strongSelf.presentationData.strings.Conversation_FileOpenIn - } - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - - var items: [ActionSheetItem] = [] - items.append(ActionSheetTextItem(title: cleanUrl)) - if hasOpenAction { - items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - if canOpenIn { - strongSelf.openUrlIn(url) - } else { - strongSelf.openUrl(url, concealed: false) - } - } - })) - } - if let phoneNumber = phoneNumber { - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddContact, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.controllerInteraction?.addContact(phoneNumber) - } - })) - } - items.append(ActionSheetButtonItem(title: canAddToReadingList ? strongSelf.presentationData.strings.ShareMenu_CopyShareLink : strongSelf.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet, weak self] in - actionSheet?.dismissAnimated() - UIPasteboard.general.string = cleanUrl - - let content: UndoOverlayContent - if isPhoneNumber { - content = .copy(text: presentationData.strings.Conversation_PhoneCopied) - } else if isEmail { - content = .copy(text: presentationData.strings.Conversation_EmailCopied) - } else if canAddToReadingList { - content = .linkCopied(text: presentationData.strings.Conversation_LinkCopied) - } else { - content = .copy(text: presentationData.strings.Conversation_TextCopied) - } - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - })) - if canAddToReadingList { - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let link = URL(string: url) { - let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) - } - })) - } - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(actionSheet, in: .window(.root)) - case let .peerMention(peerId, mention): - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - var items: [ActionSheetItem] = [] - if !mention.isEmpty { - items.append(ActionSheetTextItem(title: mention)) - } - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> deliverOnMainQueue).startStandalone(next: { peer in - if let strongSelf = self, let peer = peer { - strongSelf.openPeer(peer: peer, navigation: .chat(textInputState: nil, subject: nil, peekData: nil), fromMessage: nil) - } - }) - } - })) - if !mention.isEmpty { - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - UIPasteboard.general.string = mention - - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - })) - } - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(actionSheet, in: .window(.root)) - case let .mention(mention): - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: mention), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.openPeerMention(mention, sourceMessageId: message?.id) - } - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - UIPasteboard.general.string = mention - - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_UsernameCopied) - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(actionSheet, in: .window(.root)) - case let .command(command): - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - var items: [ActionSheetItem] = [] - items.append(ActionSheetTextItem(title: command)) - if canSendMessagesToChat(strongSelf.presentationInterfaceState) { - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.sendMessages([.message(text: command, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) - } - })) - } - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - UIPasteboard.general.string = command - - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - })) - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(actionSheet, in: .window(.root)) - case let .hashtag(hashtag): - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: hashtag), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.openHashtag(hashtag, peerName: nil) - } - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - UIPasteboard.general.string = hashtag - - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_HashtagCopied) - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(actionSheet, in: .window(.root)) - case let .timecode(timecode, text): - guard let message = message else { - return - } - - let context = strongSelf.context - let chatPresentationInterfaceState = strongSelf.presentationInterfaceState - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - - var isCopyLink = false - var isForward = false - if message.id.namespace == Namespaces.Message.Cloud, let _ = message.peers[message.id.peerId] as? TelegramChannel, !(message.media.first is TelegramMediaAction) { - isCopyLink = true - } else if let forwardInfo = message.forwardInfo, let _ = forwardInfo.author as? TelegramChannel { - isCopyLink = true - isForward = true - } - - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: text), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.controllerInteraction?.seekToTimecode(message, timecode, true) - } - }), - ActionSheetButtonItem(title: isCopyLink ? strongSelf.presentationData.strings.Conversation_ContextMenuCopyLink : strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - - var messageId = message.id - var channel = message.peers[message.id.peerId] - if isForward, let forwardMessageId = message.forwardInfo?.sourceMessageId, let forwardAuthor = message.forwardInfo?.author as? TelegramChannel { - messageId = forwardMessageId - channel = forwardAuthor - } - - if isCopyLink, let channel = channel as? TelegramChannel { - var threadId: Int64? - - if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation { - threadId = replyThreadMessage.threadId - } - let _ = (context.engine.messages.exportMessageLink(peerId: messageId.peerId, messageId: messageId, isThread: threadId != nil) - |> map { result -> String? in - return result - } - |> deliverOnMainQueue).startStandalone(next: { link in - if let link = link { - UIPasteboard.general.string = link + "?t=\(Int32(timecode))" - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - var warnAboutPrivate = false - if case .peer = chatPresentationInterfaceState.chatLocation { - if channel.addressName == nil { - warnAboutPrivate = true - } - } - Queue.mainQueue().after(0.2, { - let content: UndoOverlayContent - if warnAboutPrivate { - content = .linkCopied(text: presentationData.strings.Conversation_PrivateMessageLinkCopiedLong) - } else { - content = .linkCopied(text: presentationData.strings.Conversation_LinkCopied) - } - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - }) - } else { - UIPasteboard.general.string = text - - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - } - }) - } else { - UIPasteboard.general.string = text - - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - } - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(actionSheet, in: .window(.root)) - case let .bankCard(number): - guard let message = message else { - return - } - - var signal = strongSelf.context.engine.payments.getBankCardInfo(cardNumber: number) - let disposable: MetaDisposable - if let current = strongSelf.bankCardDisposable { - disposable = current - } else { - disposable = MetaDisposable() - strongSelf.bankCardDisposable = disposable - } - - var cancelImpl: (() -> Void)? - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let progressSignal = Signal { subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.startStrict() - - signal = signal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - cancelImpl = { - disposable.set(nil) - } - disposable.set((signal - |> deliverOnMainQueue).startStrict(next: { [weak self] info in - if let strongSelf = self, let info = info { - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - var items: [ActionSheetItem] = [] - items.append(ActionSheetTextItem(title: info.title)) - for url in info.urls { - items.append(ActionSheetButtonItem(title: url.title, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: url.url, concealed: false, external: false, message: message)) - } - })) - } - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - UIPasteboard.general.string = number - - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_CardNumberCopied) - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - })) - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.present(actionSheet, in: .window(.root)) - } - })) - - strongSelf.chatDisplayNode.dismissInput() - } + }, longTap: { [weak self] action, params in + if let self { + self.openLinkLongTap(action, params: params) } - }, openCheckoutOrReceipt: { [weak self] messageId in + }, openCheckoutOrReceipt: { [weak self] messageId, params in guard let strongSelf = self else { return } @@ -2944,7 +2660,48 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } for media in message.media { - if let invoice = media as? TelegramMediaInvoice { + if let paidContent = media as? TelegramMediaPaidContent { + let progressSignal = Signal { _ in + params?.progress?.set(.single(true)) + return ActionDisposable { + params?.progress?.set(.single(false)) + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.25, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.startStrict() + + strongSelf.chatDisplayNode.dismissInput() + let inputData = Promise() + inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, source: .message(message.id)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + }) + if let starsContext = strongSelf.context.starsContext { + let starsInputData = combineLatest( + inputData.get(), + starsContext.state + ) + |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in + if let data, let state { + return (state, data.form, data.botPeer) + } else { + return nil + } + } + let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let strongSelf = self, let extendedMedia = paidContent.extendedMedia.first, case let .preview(dimensions, immediateThumbnailData, _) = extendedMedia else { + return + } + let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: paidContent.amount, startParam: "", extendedMedia: .preview(dimensions:dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: nil), flags: [], version: 0) + let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(messageId), extendedMedia: paidContent.extendedMedia, inputData: starsInputData, completion: { _ in }) + strongSelf.push(controller) + + progressDisposable.dispose() + }) + } + } else if let invoice = media as? TelegramMediaInvoice { strongSelf.chatDisplayNode.dismissInput() if let receiptMessageId = invoice.receiptMessageId { if invoice.currency == "XTR" { @@ -2981,7 +2738,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(messageId), inputData: starsInputData, completion: { _ in }) + let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(messageId), extendedMedia: [], inputData: starsInputData, completion: { _ in }) strongSelf.push(controller) }) } else { @@ -3414,7 +3171,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, saveInterfaceState: strongSelf.presentationInterfaceState.subject != .scheduledMessages, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } }) if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { @@ -3596,7 +3353,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G completion(nil) } } else { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil) }) }, completion: completion) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) }) }, completion: completion) } } }, displayImportedMessageTooltip: { [weak self] _ in @@ -3980,223 +3737,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } strongSelf.openResolved(result: .join(joinHash), sourceMessageId: nil) }, openWebView: { [weak self] buttonText, url, simple, source in - guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { + guard let self else { return } - - strongSelf.chatDisplayNode.dismissInput() - - let botName: String - let botAddress: String - if case let .inline(bot) = source { - botName = bot.compactDisplayTitle - botAddress = bot.addressName ?? "" - } else { - botName = EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder) - botAddress = peer.addressName ?? "" - } - - if source == .generic { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedTitlePanelContext { - if !$0.contains(where: { - switch $0 { - case .requestInProgress: - return true - default: - return false - } - }) { - var updatedContexts = $0 - updatedContexts.append(.requestInProgress) - return updatedContexts.sorted() - } - return $0 - } - }) - } - - let updateProgress = { [weak self] in - Queue.mainQueue().async { - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedTitlePanelContext { - if let index = $0.firstIndex(where: { - switch $0 { - case .requestInProgress: - return true - default: - return false - } - }) { - var updatedContexts = $0 - updatedContexts.remove(at: index) - return updatedContexts - } - return $0 - } - }) - } - } - } - - let openWebView = { - if source == .menu { - strongSelf.updateChatPresentationInterfaceState(interactive: false) { state in - return state.updatedShowWebView(true).updatedForceInputCommandsHidden(true) - } - - let params = WebAppParameters(source: .menu, peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false) - let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in - self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) - }, requestSwitchInline: { [weak self] query, chatTypes, completion in - if let strongSelf = self { - if let chatTypes { - let controller = strongSelf.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: strongSelf.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: chatTypes, hasContactSelector: false, hasCreation: false)) - controller.peerSelected = { [weak self, weak controller] peer, _ in - if let strongSelf = self { - completion() - controller?.dismiss() - strongSelf.controllerInteraction?.activateSwitchInline(peer.id, "@\(botAddress) \(query)", nil) - } - } - strongSelf.push(controller) - } else { - strongSelf.controllerInteraction?.activateSwitchInline(peerId, "@\(botAddress) \(query)", nil) - } - } - }, getInputContainerNode: { [weak self] in - if let strongSelf = self, let layout = strongSelf.validLayout, case .compact = layout.metrics.widthClass { - return (strongSelf.chatDisplayNode.getWindowInputAccessoryHeight(), strongSelf.chatDisplayNode.inputPanelContainerNode, { - return strongSelf.chatDisplayNode.textInputPanelNode?.makeAttachmentMenuTransition(accessoryPanelNode: nil) - }) - } else { - return nil - } - }, completion: { [weak self] in - self?.chatDisplayNode.historyNode.scrollToEndOfHistory() - }, willDismiss: { [weak self] in - self?.interfaceInteraction?.updateShowWebView { _ in - return false - } - }, didDismiss: { [weak self] in - if let strongSelf = self { - let isFocused = strongSelf.chatDisplayNode.textInputPanelNode?.isFocused ?? false - strongSelf.chatDisplayNode.insertSubnode(strongSelf.chatDisplayNode.inputPanelContainerNode, aboveSubnode: strongSelf.chatDisplayNode.inputContextPanelContainer) - if isFocused { - strongSelf.chatDisplayNode.textInputPanelNode?.ensureFocused() - } - - strongSelf.updateChatPresentationInterfaceState(interactive: false) { state in - return state.updatedForceInputCommandsHidden(false) - } - } - }, getNavigationController: { [weak self] in - return self?.effectiveNavigationController - }) - controller.navigationPresentation = .flatModal - strongSelf.push(controller) - strongSelf.currentMenuWebAppController = controller - } else if simple { - var isInline = false - var botId = peerId - var botName = botName - var botAddress = "" - if case let .inline(bot) = source { - isInline = true - botId = bot.id - botName = bot.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder) - botAddress = bot.addressName ?? "" - } - - strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme)) - |> afterDisposed { - updateProgress() - }) - |> deliverOnMainQueue).startStrict(next: { [weak self] url in - guard let strongSelf = self else { - return - } - let params = WebAppParameters(source: isInline ? .inline : .simple, peerId: peerId, botId: botId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false) - let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in - self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) - }, requestSwitchInline: { [weak self] query, chatTypes, completion in - if let strongSelf = self { - if let chatTypes { - let controller = strongSelf.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: strongSelf.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: chatTypes, hasContactSelector: false, hasCreation: false)) - controller.peerSelected = { [weak self, weak controller] peer, _ in - if let strongSelf = self { - completion() - controller?.dismiss() - strongSelf.controllerInteraction?.activateSwitchInline(peer.id, "@\(botAddress) \(query)", nil) - } - } - strongSelf.push(controller) - } else { - strongSelf.controllerInteraction?.activateSwitchInline(peerId, "@\(botAddress) \(query)", nil) - } - } - }, getNavigationController: { [weak self] in - return self?.effectiveNavigationController - }) - controller.navigationPresentation = .flatModal - strongSelf.currentWebAppController = controller - strongSelf.push(controller) - }, error: { [weak self] error in - if let strongSelf = self { - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) - } - })) - } else { - strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestWebView(peerId: peerId, botId: peerId, url: !url.isEmpty ? url : nil, payload: nil, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme), fromMenu: buttonText == "Menu", replyToMessageId: nil, threadId: strongSelf.chatLocation.threadId) - |> afterDisposed { - updateProgress() - }) - |> deliverOnMainQueue).startStrict(next: { [weak self] result in - guard let strongSelf = self else { - return - } - let params = WebAppParameters(source: .generic, peerId: peerId, botId: peerId, botName: botName, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false) - let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in - self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) - }, completion: { [weak self] in - self?.chatDisplayNode.historyNode.scrollToEndOfHistory() - }, getNavigationController: { [weak self] in - return self?.effectiveNavigationController - }) - controller.navigationPresentation = .flatModal - strongSelf.currentWebAppController = controller - strongSelf.push(controller) - }, error: { [weak self] error in - if let strongSelf = self { - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) - } - })) - } - } - - var botPeer = EnginePeer(peer) - if case let .inline(bot) = source { - botPeer = bot - } - let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: botPeer.id) - |> deliverOnMainQueue).startStandalone(next: { value in - guard let strongSelf = self else { - return - } - - if value { - openWebView() - } else { - let controller = webAppLaunchConfirmationController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: botPeer, completion: { _ in - let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() - openWebView() - }, showMore: nil) - strongSelf.present(controller, in: .window(.root)) - } - }) + self.openWebApp(buttonText: buttonText, url: url, simple: simple, source: source) }, activateAdAction: { [weak self] messageId, progress in guard let self, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId), let adAttribute = message.adAttribute else { return @@ -4650,7 +4194,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing)) + let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) var items: [ContextMenuItem] = [ @@ -4731,21 +4275,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } self.openStickerEditor() - }, openPhoneContextMenu: { [weak self] phoneData in - guard let self else { - return - } - phoneData.progress?.set(.single(true)) - - let _ = (self.context.engine.peers.resolvePeerByPhone(phone: phoneData.number) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self else { - return - } - phoneData.progress?.set(.single(false)) - - self.openPhoneContextMenu(number: phoneData.number, peer: peer, message: phoneData.message, contentNode: phoneData.contentNode, messageNode: phoneData.messageNode, frame: phoneData.messageNode.bounds, anyRecognizer: nil, location: nil) - }) }, openAgeRestrictedMessageMedia: { [weak self] message, reveal in guard let self else { return @@ -5355,102 +4884,100 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G isScheduledMessages = true } - if let peer = peerViewMainPeer(peerView) { - if case let .messageOptions(_, _, info) = presentationInterfaceState.subject { - if case .reply = info { - let titleContent: ChatTitleContent - if case let .reply(hasQuote) = messageOptionsTitleInfo, hasQuote { - titleContent = .custom(presentationInterfaceState.strings.Chat_TitleQuoteSelection, subtitleText, false) - } else { - titleContent = .custom(presentationInterfaceState.strings.Chat_TitleReply, subtitleText, false) - } - if strongSelf.chatTitleView?.titleContent != titleContent { - if strongSelf.chatTitleView?.titleContent != nil { - strongSelf.chatTitleView?.animateLayoutTransition() - } - strongSelf.chatTitleView?.titleContent = titleContent - } - } else if case .link = info { - strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Chat_TitleLinkOptions, subtitleText, false) - } else if displayedCount == 1 { - strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_ForwardOptions_ForwardTitleSingle, subtitleText, false) + if case let .messageOptions(_, _, info) = presentationInterfaceState.subject { + if case .reply = info { + let titleContent: ChatTitleContent + if case let .reply(hasQuote) = messageOptionsTitleInfo, hasQuote { + titleContent = .custom(presentationInterfaceState.strings.Chat_TitleQuoteSelection, subtitleText, false) } else { - strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_ForwardOptions_ForwardTitle(Int32(displayedCount ?? 1)), subtitleText, false) + titleContent = .custom(presentationInterfaceState.strings.Chat_TitleReply, subtitleText, false) } - } else if let selectionState = presentationInterfaceState.interfaceState.selectionState { - if selectionState.selectedIds.count > 0 { - strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_SelectedMessages(Int32(selectionState.selectedIds.count)), nil, false) - } else { - if let reportReason = presentationInterfaceState.reportReason { - let title: String - switch reportReason { - case .spam: - title = presentationInterfaceState.strings.ReportPeer_ReasonSpam - case .fake: - title = presentationInterfaceState.strings.ReportPeer_ReasonFake - case .violence: - title = presentationInterfaceState.strings.ReportPeer_ReasonViolence - case .porno: - title = presentationInterfaceState.strings.ReportPeer_ReasonPornography - case .childAbuse: - title = presentationInterfaceState.strings.ReportPeer_ReasonChildAbuse - case .copyright: - title = presentationInterfaceState.strings.ReportPeer_ReasonCopyright - case .illegalDrugs: - title = presentationInterfaceState.strings.ReportPeer_ReasonIllegalDrugs - case .personalDetails: - title = presentationInterfaceState.strings.ReportPeer_ReasonPersonalDetails - case .custom: - title = presentationInterfaceState.strings.ReportPeer_ReasonOther - case .irrelevantLocation: - title = "" - } - strongSelf.chatTitleView?.titleContent = .custom(title, presentationInterfaceState.strings.Conversation_SelectMessages, false) - } else { - strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_SelectMessages, nil, false) + if strongSelf.chatTitleView?.titleContent != titleContent { + if strongSelf.chatTitleView?.titleContent != nil { + strongSelf.chatTitleView?.animateLayoutTransition() } + strongSelf.chatTitleView?.titleContent = titleContent } + } else if case .link = info { + strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Chat_TitleLinkOptions, subtitleText, false) + } else if displayedCount == 1 { + strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_ForwardOptions_ForwardTitleSingle, subtitleText, false) } else { - if case .pinnedMessages = presentationInterfaceState.subject { - strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Chat_TitlePinnedMessages(Int32(displayedCount ?? 1)), nil, false) - } else { - strongSelf.chatTitleView?.titleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages, isMuted: nil, customMessageCount: nil, isEnabled: hasPeerInfo) - let imageOverride: AvatarNodeImageOverride? - if strongSelf.context.account.peerId == peer.id { - imageOverride = .savedMessagesIcon - } else if peer.id.isReplies { - imageOverride = .repliesIcon - } else if peer.id.isAnonymousSavedMessages { - imageOverride = .anonymousSavedMessagesIcon - } else if peer.isDeleted { - imageOverride = .deletedIcon - } else { - imageOverride = nil - } - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: EnginePeer(peer), overrideImage: imageOverride) - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = strongSelf.chatLocation.threadId == nil && peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil - strongSelf.chatInfoNavigationButton?.buttonItem.accessibilityLabel = presentationInterfaceState.strings.Conversation_ContextMenuOpenProfile - - strongSelf.storyStats = peerView.storyStats - if let avatarNode = strongSelf.avatarNode { - avatarNode.avatarNode.setStoryStats(storyStats: peerView.storyStats.flatMap { storyStats -> AvatarNode.StoryStats? in - if storyStats.totalCount == 0 { - return nil - } - if storyStats.unseenCount == 0 { - return nil - } - return AvatarNode.StoryStats( - totalCount: storyStats.totalCount, - unseenCount: storyStats.unseenCount, - hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriends - ) - }, presentationParams: AvatarNode.StoryPresentationParams( - colors: AvatarNode.Colors(theme: strongSelf.presentationData.theme), - lineWidth: 1.5, - inactiveLineWidth: 1.5 - ), transition: .immediate) + strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_ForwardOptions_ForwardTitle(Int32(displayedCount ?? 1)), subtitleText, false) + } + } else if let selectionState = presentationInterfaceState.interfaceState.selectionState { + if selectionState.selectedIds.count > 0 { + strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_SelectedMessages(Int32(selectionState.selectedIds.count)), nil, false) + } else { + if let reportReason = presentationInterfaceState.reportReason { + let title: String + switch reportReason { + case .spam: + title = presentationInterfaceState.strings.ReportPeer_ReasonSpam + case .fake: + title = presentationInterfaceState.strings.ReportPeer_ReasonFake + case .violence: + title = presentationInterfaceState.strings.ReportPeer_ReasonViolence + case .porno: + title = presentationInterfaceState.strings.ReportPeer_ReasonPornography + case .childAbuse: + title = presentationInterfaceState.strings.ReportPeer_ReasonChildAbuse + case .copyright: + title = presentationInterfaceState.strings.ReportPeer_ReasonCopyright + case .illegalDrugs: + title = presentationInterfaceState.strings.ReportPeer_ReasonIllegalDrugs + case .personalDetails: + title = presentationInterfaceState.strings.ReportPeer_ReasonPersonalDetails + case .custom: + title = presentationInterfaceState.strings.ReportPeer_ReasonOther + case .irrelevantLocation: + title = "" } + strongSelf.chatTitleView?.titleContent = .custom(title, presentationInterfaceState.strings.Conversation_SelectMessages, false) + } else { + strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_SelectMessages, nil, false) + } + } + } else if let peer = peerViewMainPeer(peerView) { + if case .pinnedMessages = presentationInterfaceState.subject { + strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Chat_TitlePinnedMessages(Int32(displayedCount ?? 1)), nil, false) + } else { + strongSelf.chatTitleView?.titleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages, isMuted: nil, customMessageCount: nil, isEnabled: hasPeerInfo) + let imageOverride: AvatarNodeImageOverride? + if strongSelf.context.account.peerId == peer.id { + imageOverride = .savedMessagesIcon + } else if peer.id.isReplies { + imageOverride = .repliesIcon + } else if peer.id.isAnonymousSavedMessages { + imageOverride = .anonymousSavedMessagesIcon + } else if peer.isDeleted { + imageOverride = .deletedIcon + } else { + imageOverride = nil + } + (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: EnginePeer(peer), overrideImage: imageOverride) + (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = strongSelf.chatLocation.threadId == nil && peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil + strongSelf.chatInfoNavigationButton?.buttonItem.accessibilityLabel = presentationInterfaceState.strings.Conversation_ContextMenuOpenProfile + + strongSelf.storyStats = peerView.storyStats + if let avatarNode = strongSelf.avatarNode { + avatarNode.avatarNode.setStoryStats(storyStats: peerView.storyStats.flatMap { storyStats -> AvatarNode.StoryStats? in + if storyStats.totalCount == 0 { + return nil + } + if storyStats.unseenCount == 0 { + return nil + } + return AvatarNode.StoryStats( + totalCount: storyStats.totalCount, + unseenCount: storyStats.unseenCount, + hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriends + ) + }, presentationParams: AvatarNode.StoryPresentationParams( + colors: AvatarNode.Colors(theme: strongSelf.presentationData.theme), + lineWidth: 1.5, + inactiveLineWidth: 1.5 + ), transition: .immediate) } } } @@ -6807,82 +6334,85 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var presentationData = presentationData var useDarkAppearance = presentationData.theme.overallDarkAppearance - if let wallpaper = chatWallpaper, case let .emoticon(wallpaperEmoticon) = wallpaper, let theme = chatThemes.first(where: { $0.emoticon?.strippedEmoji == wallpaperEmoticon.strippedEmoji }) { - let themeSettings: TelegramThemeSettings? - if let matching = theme.settings?.first(where: { $0.baseTheme == presentationData.theme.referenceTheme.baseTheme }) { - themeSettings = matching - } else { - themeSettings = theme.settings?.first - } - if let themeWallpaper = themeSettings?.wallpaper { - chatWallpaper = themeWallpaper - } - } - if let themeEmoticon = themeEmoticon, let theme = chatThemes.first(where: { $0.emoticon?.strippedEmoji == themeEmoticon.strippedEmoji }) { - if let darkAppearancePreview = darkAppearancePreview { - useDarkAppearance = darkAppearancePreview - } - if let theme = makePresentationTheme(cloudTheme: theme, dark: useDarkAppearance) { - theme.forceSync = true - presentationData = presentationData.withUpdated(theme: theme).withUpdated(chatWallpaper: theme.chat.defaultWallpaper) - - Queue.mainQueue().after(1.0, { - theme.forceSync = false - }) - } - } else if let darkAppearancePreview = darkAppearancePreview { - useDarkAppearance = darkAppearancePreview - let lightTheme: PresentationTheme - let lightWallpaper: TelegramWallpaper - - let darkTheme: PresentationTheme - let darkWallpaper: TelegramWallpaper - - if presentationData.autoNightModeTriggered { - darkTheme = presentationData.theme - darkWallpaper = presentationData.chatWallpaper - - var currentColors = themeSettings.themeSpecificAccentColors[themeSettings.theme.index] - if let colors = currentColors, colors.baseColor == .theme { - currentColors = nil - } - - let themeSpecificWallpaper = (themeSettings.themeSpecificChatWallpapers[coloredThemeIndex(reference: themeSettings.theme, accentColor: currentColors)] ?? themeSettings.themeSpecificChatWallpapers[themeSettings.theme.index]) - - if let themeSpecificWallpaper = themeSpecificWallpaper { - lightWallpaper = themeSpecificWallpaper + if let forcedTheme = strongSelf.forcedTheme { + presentationData = presentationData.withUpdated(theme: forcedTheme) + } else { + if let wallpaper = chatWallpaper, case let .emoticon(wallpaperEmoticon) = wallpaper, let theme = chatThemes.first(where: { $0.emoticon?.strippedEmoji == wallpaperEmoticon.strippedEmoji }) { + let themeSettings: TelegramThemeSettings? + if let matching = theme.settings?.first(where: { $0.baseTheme == presentationData.theme.referenceTheme.baseTheme }) { + themeSettings = matching } else { - let theme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: themeSettings.theme, accentColor: currentColors?.color, bubbleColors: currentColors?.customBubbleColors ?? [], wallpaper: currentColors?.wallpaper, baseColor: currentColors?.baseColor, preview: true) ?? defaultPresentationTheme - lightWallpaper = theme.chat.defaultWallpaper + themeSettings = theme.settings?.first } - - var preferredBaseTheme: TelegramBaseTheme? - if let baseTheme = themeSettings.themePreferredBaseTheme[themeSettings.theme.index], [.classic, .day].contains(baseTheme) { - preferredBaseTheme = baseTheme + if let themeWallpaper = themeSettings?.wallpaper { + chatWallpaper = themeWallpaper } - - lightTheme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: themeSettings.theme, baseTheme: preferredBaseTheme, accentColor: currentColors?.color, bubbleColors: currentColors?.customBubbleColors ?? [], wallpaper: currentColors?.wallpaper, baseColor: currentColors?.baseColor, serviceBackgroundColor: defaultServiceBackgroundColor) ?? defaultPresentationTheme - } else { - lightTheme = presentationData.theme - lightWallpaper = presentationData.chatWallpaper - - let automaticTheme = themeSettings.automaticThemeSwitchSetting.theme - let effectiveColors = themeSettings.themeSpecificAccentColors[automaticTheme.index] - let themeSpecificWallpaper = (themeSettings.themeSpecificChatWallpapers[coloredThemeIndex(reference: automaticTheme, accentColor: effectiveColors)] ?? themeSettings.themeSpecificChatWallpapers[automaticTheme.index]) - - var preferredBaseTheme: TelegramBaseTheme? - if let baseTheme = themeSettings.themePreferredBaseTheme[automaticTheme.index], [.night, .tinted].contains(baseTheme) { - preferredBaseTheme = baseTheme - } else { - preferredBaseTheme = .night + } + if let themeEmoticon = themeEmoticon, let theme = chatThemes.first(where: { $0.emoticon?.strippedEmoji == themeEmoticon.strippedEmoji }) { + if let darkAppearancePreview = darkAppearancePreview { + useDarkAppearance = darkAppearancePreview } + if let theme = makePresentationTheme(cloudTheme: theme, dark: useDarkAppearance) { + theme.forceSync = true + presentationData = presentationData.withUpdated(theme: theme).withUpdated(chatWallpaper: theme.chat.defaultWallpaper) + + Queue.mainQueue().after(1.0, { + theme.forceSync = false + }) + } + } else if let darkAppearancePreview = darkAppearancePreview { + useDarkAppearance = darkAppearancePreview + let lightTheme: PresentationTheme + let lightWallpaper: TelegramWallpaper - darkTheme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: automaticTheme, baseTheme: preferredBaseTheme, accentColor: effectiveColors?.color, bubbleColors: effectiveColors?.customBubbleColors ?? [], wallpaper: effectiveColors?.wallpaper, baseColor: effectiveColors?.baseColor, serviceBackgroundColor: defaultServiceBackgroundColor) ?? defaultPresentationTheme + let darkTheme: PresentationTheme + let darkWallpaper: TelegramWallpaper - if let themeSpecificWallpaper = themeSpecificWallpaper { - darkWallpaper = themeSpecificWallpaper + if presentationData.autoNightModeTriggered { + darkTheme = presentationData.theme + darkWallpaper = presentationData.chatWallpaper + + var currentColors = themeSettings.themeSpecificAccentColors[themeSettings.theme.index] + if let colors = currentColors, colors.baseColor == .theme { + currentColors = nil + } + + let themeSpecificWallpaper = (themeSettings.themeSpecificChatWallpapers[coloredThemeIndex(reference: themeSettings.theme, accentColor: currentColors)] ?? themeSettings.themeSpecificChatWallpapers[themeSettings.theme.index]) + + if let themeSpecificWallpaper = themeSpecificWallpaper { + lightWallpaper = themeSpecificWallpaper + } else { + let theme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: themeSettings.theme, accentColor: currentColors?.color, bubbleColors: currentColors?.customBubbleColors ?? [], wallpaper: currentColors?.wallpaper, baseColor: currentColors?.baseColor, preview: true) ?? defaultPresentationTheme + lightWallpaper = theme.chat.defaultWallpaper + } + + var preferredBaseTheme: TelegramBaseTheme? + if let baseTheme = themeSettings.themePreferredBaseTheme[themeSettings.theme.index], [.classic, .day].contains(baseTheme) { + preferredBaseTheme = baseTheme + } + + lightTheme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: themeSettings.theme, baseTheme: preferredBaseTheme, accentColor: currentColors?.color, bubbleColors: currentColors?.customBubbleColors ?? [], wallpaper: currentColors?.wallpaper, baseColor: currentColors?.baseColor, serviceBackgroundColor: defaultServiceBackgroundColor) ?? defaultPresentationTheme } else { - switch lightWallpaper { + lightTheme = presentationData.theme + lightWallpaper = presentationData.chatWallpaper + + let automaticTheme = themeSettings.automaticThemeSwitchSetting.theme + let effectiveColors = themeSettings.themeSpecificAccentColors[automaticTheme.index] + let themeSpecificWallpaper = (themeSettings.themeSpecificChatWallpapers[coloredThemeIndex(reference: automaticTheme, accentColor: effectiveColors)] ?? themeSettings.themeSpecificChatWallpapers[automaticTheme.index]) + + var preferredBaseTheme: TelegramBaseTheme? + if let baseTheme = themeSettings.themePreferredBaseTheme[automaticTheme.index], [.night, .tinted].contains(baseTheme) { + preferredBaseTheme = baseTheme + } else { + preferredBaseTheme = .night + } + + darkTheme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: automaticTheme, baseTheme: preferredBaseTheme, accentColor: effectiveColors?.color, bubbleColors: effectiveColors?.customBubbleColors ?? [], wallpaper: effectiveColors?.wallpaper, baseColor: effectiveColors?.baseColor, serviceBackgroundColor: defaultServiceBackgroundColor) ?? defaultPresentationTheme + + if let themeSpecificWallpaper = themeSpecificWallpaper { + darkWallpaper = themeSpecificWallpaper + } else { + switch lightWallpaper { case .builtin, .color, .gradient: darkWallpaper = darkTheme.chat.defaultWallpaper case .file: @@ -6893,26 +6423,29 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } default: darkWallpaper = lightWallpaper + } } } - } - - if darkAppearancePreview { - darkTheme.forceSync = true - Queue.mainQueue().after(1.0, { - darkTheme.forceSync = false - }) - presentationData = presentationData.withUpdated(theme: darkTheme).withUpdated(chatWallpaper: darkWallpaper) - } else { - lightTheme.forceSync = true - Queue.mainQueue().after(1.0, { - lightTheme.forceSync = false - }) - presentationData = presentationData.withUpdated(theme: lightTheme).withUpdated(chatWallpaper: lightWallpaper) + + if darkAppearancePreview { + darkTheme.forceSync = true + Queue.mainQueue().after(1.0, { + darkTheme.forceSync = false + }) + presentationData = presentationData.withUpdated(theme: darkTheme).withUpdated(chatWallpaper: darkWallpaper) + } else { + lightTheme.forceSync = true + Queue.mainQueue().after(1.0, { + lightTheme.forceSync = false + }) + presentationData = presentationData.withUpdated(theme: lightTheme).withUpdated(chatWallpaper: lightWallpaper) + } } } - if let chatWallpaper { + if let forcedWallpaper = strongSelf.forcedWallpaper { + presentationData = presentationData.withUpdated(chatWallpaper: forcedWallpaper) + } else if let chatWallpaper { presentationData = presentationData.withUpdated(chatWallpaper: chatWallpaper) } @@ -7169,16 +6702,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G func updateNavigationBarPresentation() { let navigationBarTheme: NavigationBarTheme - - if self.hasEmbeddedTitleContent { + + let presentationTheme: PresentationTheme + if let forcedNavigationBarTheme = self.forcedNavigationBarTheme { + presentationTheme = forcedNavigationBarTheme + navigationBarTheme = NavigationBarTheme(rootControllerTheme: forcedNavigationBarTheme, hideBackground: false, hideBadge: true) + } else if self.hasEmbeddedTitleContent { + presentationTheme = self.presentationData.theme navigationBarTheme = NavigationBarTheme(rootControllerTheme: defaultDarkPresentationTheme, hideBackground: self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding ? true : false, hideBadge: true) } else { + presentationTheme = self.presentationData.theme navigationBarTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme, hideBackground: self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding ? true : false, hideBadge: false) } self.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) - self.chatTitleView?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings, hasEmbeddedTitleContent: self.hasEmbeddedTitleContent) + self.chatTitleView?.updateThemeAndStrings(theme: presentationTheme, strings: self.presentationData.strings, hasEmbeddedTitleContent: self.hasEmbeddedTitleContent) } func topPinnedMessageSignal(latest: Bool) -> Signal { @@ -7269,7 +6808,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G chatLocation = .peer(id: peerId) } - return (chatHistoryViewForLocation(ChatHistoryLocationInput(content: location, id: 0), ignoreMessagesInTimestampRange: nil, context: context, chatLocation: chatLocation, chatLocationContextHolder: Atomic(value: nil), scheduled: false, fixedCombinedReadStates: nil, tag: .tag(MessageTags.pinned), appendMessagesFromTheSameGroup: false, additionalData: [], orderStatistics: .combinedLocation) + return (chatHistoryViewForLocation(ChatHistoryLocationInput(content: location, id: 0), ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), context: context, chatLocation: chatLocation, chatLocationContextHolder: Atomic(value: nil), scheduled: false, fixedCombinedReadStates: nil, tag: .tag(MessageTags.pinned), appendMessagesFromTheSameGroup: false, additionalData: [], orderStatistics: .combinedLocation) |> castError(Bool.self) |> mapToSignal { update -> Signal in switch update { @@ -8195,7 +7734,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } interfaceState = interfaceState.withUpdatedInputLanguage(self.chatDisplayNode.currentTextInputLanguage) if case .peer = self.chatLocation, let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum) { - interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState()).withUpdatedReplyMessageSubject(nil) + interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState()).withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } let _ = ChatInterfaceState.update(engine: self.context.engine, peerId: peerId, threadId: threadId, { _ in return interfaceState @@ -8492,165 +8031,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.presentAttachmentMenu(subject: .bot(id: botId, payload: payload, justInstalled: justInstalled)) } - public func presentBotApp(botApp: BotApp, botPeer: EnginePeer, payload: String?, concealed: Bool = false, commit: @escaping () -> Void = {}) { - guard let peerId = self.chatLocation.peerId else { - return - } - self.attachmentController?.dismiss(animated: true, completion: nil) - - let openBotApp: (Bool, Bool) -> Void = { [weak self] allowWrite, justInstalled in - guard let strongSelf = self else { - return - } - commit() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedTitlePanelContext { - if !$0.contains(where: { - switch $0 { - case .requestInProgress: - return true - default: - return false - } - }) { - var updatedContexts = $0 - updatedContexts.append(.requestInProgress) - return updatedContexts.sorted() - } - return $0 - } - }) - - let updateProgress = { [weak self] in - Queue.mainQueue().async { - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedTitlePanelContext { - if let index = $0.firstIndex(where: { - switch $0 { - case .requestInProgress: - return true - default: - return false - } - }) { - var updatedContexts = $0 - updatedContexts.remove(at: index) - return updatedContexts - } - return $0 - } - }) - } - } - } - - let botAddress = botPeer.addressName ?? "" - strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestAppWebView(peerId: peerId, appReference: .id(id: botApp.id, accessHash: botApp.accessHash), payload: payload, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme), allowWrite: allowWrite) - |> afterDisposed { - updateProgress() - }) - |> deliverOnMainQueue).startStrict(next: { [weak self] url in - guard let strongSelf = self else { - return - } - let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, url: url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings)) - let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in - self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) - }, requestSwitchInline: { [weak self] query, chatTypes, completion in - if let strongSelf = self { - if let chatTypes { - let controller = strongSelf.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: strongSelf.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: chatTypes, hasContactSelector: false, hasCreation: false)) - controller.peerSelected = { [weak self, weak controller] peer, _ in - if let strongSelf = self { - completion() - controller?.dismiss() - strongSelf.controllerInteraction?.activateSwitchInline(peer.id, "@\(botAddress) \(query)", nil) - } - } - strongSelf.push(controller) - } else { - strongSelf.controllerInteraction?.activateSwitchInline(peerId, "@\(botAddress) \(query)", nil) - } - } - }, completion: { [weak self] in - self?.chatDisplayNode.historyNode.scrollToEndOfHistory() - }, getNavigationController: { [weak self] in - return self?.effectiveNavigationController - }) - controller.navigationPresentation = .flatModal - strongSelf.currentWebAppController = controller - strongSelf.push(controller) - - if justInstalled { - let content: UndoOverlayContent = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(botPeer.compactDisplayTitle).string, timeout: 5.0, customUndoText: nil) - controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current) - } - }, error: { [weak self] error in - if let strongSelf = self { - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) - } - })) - } - - let _ = combineLatest( - queue: Queue.mainQueue(), - ApplicationSpecificNotice.getBotGameNotice(accountManager: self.context.sharedContext.accountManager, peerId: botPeer.id), - self.context.engine.messages.attachMenuBots(), - self.context.engine.messages.getAttachMenuBot(botId: botPeer.id, cached: true) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - ).startStandalone(next: { [weak self] value, attachMenuBots, attachMenuBot in - guard let self else { - return - } - - var isAttachMenuBotInstalled: Bool? - if let _ = attachMenuBot { - if let _ = attachMenuBots.first(where: { $0.peer.id == botPeer.id && !$0.flags.contains(.notActivated) }) { - isAttachMenuBotInstalled = true - } else { - isAttachMenuBotInstalled = false - } - } - - let context = self.context - if !value || concealed || botApp.flags.contains(.notActivated) || isAttachMenuBotInstalled == false { - if let isAttachMenuBotInstalled, let attachMenuBot { - if !isAttachMenuBotInstalled { - let controller = webAppTermsAlertController(context: context, updatedPresentationData: self.updatedPresentationData, bot: attachMenuBot, completion: { allowWrite in - let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() - let _ = (context.engine.messages.addBotToAttachMenu(botId: botPeer.id, allowWrite: allowWrite) - |> deliverOnMainQueue).startStandalone(error: { _ in - }, completed: { - openBotApp(allowWrite, true) - }) - }) - self.present(controller, in: .window(.root)) - } else { - openBotApp(false, false) - } - } else { - let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: self.updatedPresentationData, peer: botPeer, requestWriteAccess: botApp.flags.contains(.notActivated) && botApp.flags.contains(.requiresWriteAccess), completion: { allowWrite in - let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() - openBotApp(allowWrite, false) - }, showMore: { [weak self] in - if let self { - self.openResolved(result: .peer(botPeer._asPeer(), .info(nil)), sourceMessageId: nil) - } - }) - self.present(controller, in: .window(.root)) - } - } else { - openBotApp(false, false) - } - }) - } - func displayPollSolution(solution: TelegramMediaPollResults.Solution, sourceNode: ASDisplayNode, isAutomatic: Bool) { var maybeFoundItemNode: ChatMessageItemView? self.chatDisplayNode.historyNode.forEachItemNode { itemNode in @@ -8973,7 +8353,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } }) } }, nil) @@ -9227,6 +8607,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G attributes.append(SendAsMessageAttribute(peerId: sendAsPeerId)) } } + if let sendMessageEffect = self.presentationInterfaceState.interfaceState.sendMessageEffect { + if attributes.first(where: { $0 is EffectMessageAttribute }) == nil { + attributes.append(EffectMessageAttribute(id: sendMessageEffect)) + } + } return attributes } } @@ -9396,7 +8781,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } }) } completionImpl?() @@ -9440,7 +8825,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if resetTextInputState { state = state.updatedInterfaceState { interfaceState in var interfaceState = interfaceState - interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil) + interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) interfaceState = interfaceState.withUpdatedComposeDisableUrlPreviews([]) return interfaceState @@ -9920,7 +9305,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId.id)) |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in if let strongSelf = self, let peer { - strongSelf.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, concealed: concealed, commit: { + strongSelf.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, compact: botAppStart.compact, concealed: concealed, commit: { dismissWebAppControllers() commit() }) diff --git a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift index 642feb20c27..e9aa686373b 100644 --- a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -319,88 +319,25 @@ extension ChatControllerImpl { )) }) })) + } + + func beginDeleteMessagesWithUndo(messageIds: Set, type: InteractiveMessagesDeletionType) { + self.chatDisplayNode.historyNode.ignoreMessageIds = Set(messageIds) - /*do { - self.navigationActionDisposable.set((self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id) - |> deliverOnMainQueue).startStrict(next: { - if let strongSelf = self { - if "".isEmpty { - - return - } - - let canBan = participant?.canBeBannedBy(peerId: accountPeerId) ?? true - - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - var items: [ActionSheetItem] = [] - - var actions = Set([0]) - - let toggleCheck: (Int, Int) -> Void = { [weak actionSheet] category, itemIndex in - if actions.contains(category) { - actions.remove(category) - } else { - actions.insert(category) - } - actionSheet?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in - if let item = item as? ActionSheetCheckboxItem { - return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) - } - return item - }) - } - - var itemIndex = 0 - var categories: [Int] = [0] - if canBan { - categories.append(1) - } - categories.append(contentsOf: [2, 3]) - - for categoryId in categories as [Int] { - var title = "" - if categoryId == 0 { - title = strongSelf.presentationData.strings.Conversation_Moderate_Delete - } else if categoryId == 1 { - title = strongSelf.presentationData.strings.Conversation_Moderate_Ban - } else if categoryId == 2 { - title = strongSelf.presentationData.strings.Conversation_Moderate_Report - } else if categoryId == 3 { - title = strongSelf.presentationData.strings.Conversation_Moderate_DeleteAllMessages(EnginePeer(author).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } - let index = itemIndex - items.append(ActionSheetCheckboxItem(title: title, label: "", value: actions.contains(categoryId), action: { value in - toggleCheck(categoryId, index) - })) - itemIndex += 1 - } - - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Done, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - if actions.contains(3) { - let _ = strongSelf.context.engine.messages.deleteAllMessagesWithAuthor(peerId: peerId, authorId: author.id, namespace: Namespaces.Message.Cloud).startStandalone() - let _ = strongSelf.context.engine.messages.clearAuthorHistory(peerId: peerId, memberId: author.id).startStandalone() - } else if actions.contains(0) { - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() - } - if actions.contains(1) { - let _ = strongSelf.context.engine.peers.removePeerMember(peerId: peerId, memberId: author.id).startStandalone() - } - } - })) - - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(actionSheet, in: .window(.root)) - } - })) - }*/ + let undoTitle = self.presentationData.strings.Chat_MessagesDeletedToast_Text(Int32(messageIds.count)) + self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: undoTitle, text: nil), elevatedLayout: false, position: .top, action: { [weak self] value in + guard let self else { + return false + } + if value == .commit { + let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: type).startStandalone() + return true + } else if value == .undo { + self.chatDisplayNode.historyNode.ignoreMessageIds = Set() + return true + } + return false + }), in: .current) } func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions, contextController: ContextControllerProtocol?, completion: @escaping (ContextMenuActionResult) -> Void) { @@ -429,7 +366,7 @@ extension ChatControllerImpl { actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() + strongSelf.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) } })) } @@ -466,7 +403,8 @@ extension ChatControllerImpl { } let commit = { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() + + strongSelf.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) } if let giveaway { let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) @@ -501,7 +439,8 @@ extension ChatControllerImpl { actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() + + strongSelf.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) } })) } @@ -534,7 +473,7 @@ extension ChatControllerImpl { guard let strongSelf = self else { return } - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone() + strongSelf.beginDeleteMessagesWithUndo(messageIds: messageIds, type: unsendPersonalMessages ? .forEveryone : .forLocalPeer) } if "".isEmpty { @@ -553,8 +492,8 @@ extension ChatControllerImpl { actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone() + strongSelf.beginDeleteMessagesWithUndo(messageIds: messageIds, type: unsendPersonalMessages ? .forEveryone : .forLocalPeer) } })) } diff --git a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift index 157cfe8420f..6996612b05e 100644 --- a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift +++ b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift @@ -93,6 +93,9 @@ extension ChatControllerImpl { filter.insert(.excludeChannels) break } + if let _ = media as? TelegramMediaPaidContent { + filter.insert(.excludeSecretChats) + } } } var attemptSelectionImpl: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 9dfdf975190..d778e225e54 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -597,6 +597,13 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { var peers = SimpleDictionary() peers[accountPeer.id] = accountPeer + let author: Peer + if link.isCentered { + author = TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + } else { + author = accountPeer + } + var associatedMessages = SimpleDictionary() var media: [Media] = [] @@ -637,7 +644,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { localTags: [], customTags: [], forwardInfo: nil, - author: accountPeer, + author: author, text: options.messageText, attributes: attributes, media: media, @@ -1893,13 +1900,19 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { transition.updateFrame(node: backgroundEffectNode, frame: CGRect(origin: CGPoint(), size: layout.size)) } - let wallpaperBounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width - wrappingInsets.left - wrappingInsets.right, height: layout.size.height) + var wallpaperBounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width - wrappingInsets.left - wrappingInsets.right, height: layout.size.height) transition.updateFrame(node: self.backgroundNode, frame: wallpaperBounds) var displayMode: WallpaperDisplayMode = .aspectFill if case .regular = layout.metrics.widthClass, layout.size.height == layout.deviceMetrics.screenSize.width { displayMode = .aspectFit + } else if case .compact = layout.metrics.widthClass { + if layout.size.width < layout.size.height && layout.size.height < layout.deviceMetrics.screenSize.height { + wallpaperBounds.size.height = layout.deviceMetrics.screenSize.height + } else if layout.size.width > layout.size.height && layout.size.height < layout.deviceMetrics.screenSize.width { + wallpaperBounds.size.height = layout.deviceMetrics.screenSize.width + } } self.backgroundNode.updateLayout(size: wallpaperBounds.size, displayMode: displayMode, transition: transition) @@ -2731,7 +2744,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { let peerId = self.chatPresentationInterfaceState.chatLocation.peerId let inlineSearchResults: ComponentView - var inlineSearchResultsTransition = Transition(transition) + var inlineSearchResultsTransition = ComponentTransition(transition) if let current = self.inlineSearchResults { inlineSearchResults = current } else { @@ -2922,6 +2935,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { return chatHistoryViewForLocation( input, ignoreMessagesInTimestampRange: nil, + ignoreMessageIds: Set(), context: context, chatLocation: chatLocation, chatLocationContextHolder: Atomic(value: nil), @@ -4396,7 +4410,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { strongSelf.ignoreUpdateHeight = true textInputPanelNode.text = "" - strongSelf.requestUpdateChatInterfaceState(.immediate, true, { $0.withUpdatedReplyMessageSubject(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeDisableUrlPreviews([]) }) + strongSelf.requestUpdateChatInterfaceState(.immediate, true, { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeDisableUrlPreviews([]) }) strongSelf.ignoreUpdateHeight = false } }, usedCorrelationId) diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index a056f61a5c2..847b3f83f1a 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -43,6 +43,10 @@ extension ChatControllerImpl { } func presentAttachmentMenu(subject: AttachMenuSubject) { + guard self.audioRecorderValue == nil && self.videoRecorderValue == nil else { + return + } + let context = self.context let inputIsActive = self.presentationInterfaceState.inputMode == .text @@ -277,6 +281,13 @@ extension ChatControllerImpl { } return EntityInputView(context: strongSelf.context, isDark: false, areCustomEmojiEnabled: strongSelf.presentationInterfaceState.customEmojiAvailable) }) + attachmentController.shouldMinimizeOnSwipe = { [weak attachmentController] button in + if case .app = button { + attachmentController?.convertToStandalone() + return true + } + return false + } attachmentController.didDismiss = { [weak self] in self?.attachmentController = nil self?.canReadHistory.set(true) @@ -384,7 +395,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } }) } }, nil) @@ -448,7 +459,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } }) } }, nil) @@ -520,7 +531,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } }) } }, nil) @@ -551,7 +562,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } }) } }, nil) @@ -604,7 +615,7 @@ extension ChatControllerImpl { payload = botPayload fromAttachMenu = false } - let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false) + let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: false) let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId) controller.openUrl = { [weak self] url, concealed, commit in @@ -616,7 +627,7 @@ extension ChatControllerImpl { controller.completion = { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } }) strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() } @@ -1116,7 +1127,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } }) } }, nil) @@ -1159,6 +1170,10 @@ extension ChatControllerImpl { if case .scheduledMessages = self.presentationInterfaceState.subject { isScheduledMessages = true } + var paidMediaAllowed = false + if let cachedData = self.peerView?.cachedData as? CachedChannelData, cachedData.flags.contains(.paidMediaAllowed) { + paidMediaAllowed = true + } let controller = MediaPickerScreen( context: self.context, updatedPresentationData: self.updatedPresentationData, @@ -1169,6 +1184,7 @@ extension ChatControllerImpl { bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, canBoostToUnrestrict: (self.presentationInterfaceState.boostsToUnrestrict ?? 0) > 0 && bannedSendPhotos?.1 != true && bannedSendVideos?.1 != true, + paidMediaAllowed: paidMediaAllowed, subject: subject, saveEditedPhotos: saveEditedPhotos ) @@ -1491,7 +1507,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } }) } }, nil) @@ -1540,7 +1556,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } }) } }, nil) @@ -1601,7 +1617,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } }) } }, nil) @@ -1621,7 +1637,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } }) } }, nil) diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift index c0e39f7d9f0..2a05e3a16bf 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift @@ -128,7 +128,7 @@ extension ChatControllerImpl { }))) } - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .message(id: .timestamp(timestamp), highlight: nil, timecode: nil), botStart: nil, mode: .standard(.previewing)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .message(id: .timestamp(timestamp), highlight: nil, timecode: nil), botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index e36db96f85e..f65710f110b 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -15,6 +15,8 @@ import TextNodeWithEntities import ChatPresentationInterfaceState import SavedTagNameAlertController import PremiumUI +import ChatSendStarsScreen +import ChatMessageItemCommon extension ChatControllerImpl { func presentTagPremiumPaywall() { @@ -159,6 +161,41 @@ extension ChatControllerImpl { self.window?.presentInGlobalOverlay(controller) }) } else { + if self.context.sharedContext.applicationBindings.appBuildType == .internal, case .custom(MessageReaction.starsReactionId) = value { + let _ = (ChatSendStarsScreen.initialData(context: self.context, peerId: message.id.peerId) + |> deliverOnMainQueue).start(next: { [weak self] initialData in + guard let self, let initialData else { + return + } + self.push(ChatSendStarsScreen(context: self.context, initialData: initialData, completion: { [weak self] amount in + guard let self else { + return + } + + let _ = (self.context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId]) + |> deliverOnMainQueue).start(next: { [weak self] files in + guard let self, let file = files[MessageReaction.starsReactionId] else { + return + } + + //TODO:localize + let title: String + if amount == 1 { + title = "Star Sent" + } else { + title = "\(amount) Stars Sent" + } + + self.present(UndoOverlayController(presentationData: self.presentationData, content: .starsSent(context: self.context, file: file, amount: amount, title: title, text: nil), elevatedLayout: false, action: { _ in + return false + }), in: .current) + }) + })) + }) + + return + } + var customFileIds: [Int64] = [] if case let .custom(fileId) = value { customFileIds.append(fileId) @@ -175,21 +212,28 @@ extension ChatControllerImpl { var dismissController: ((@escaping () -> Void) -> Void)? - var items = ContextController.Items(content: .custom(ReactionListContextMenuContent( - context: self.context, - displayReadTimestamps: false, - availableReactions: availableReactions, - animationCache: self.controllerInteraction!.presentationContext.animationCache, - animationRenderer: self.controllerInteraction!.presentationContext.animationRenderer, - message: EngineMessage(message), reaction: value, readStats: nil, back: nil, openPeer: { peer, hasReaction in - dismissController?({ [weak self] in - guard let self else { - return + var items: ContextController.Items + if canViewMessageReactionList(message: message) { + items = ContextController.Items(content: .custom(ReactionListContextMenuContent( + context: self.context, + displayReadTimestamps: false, + availableReactions: availableReactions, + animationCache: self.controllerInteraction!.presentationContext.animationCache, + animationRenderer: self.controllerInteraction!.presentationContext.animationRenderer, + message: EngineMessage(message), + reaction: value, readStats: nil, back: nil, openPeer: { peer, hasReaction in + dismissController?({ [weak self] in + guard let self else { + return + } + + self.openPeer(peer: peer, navigation: .default, fromMessage: MessageReference(message), fromReactionMessageId: hasReaction ? message.id : nil) + }) } - - self.openPeer(peer: peer, navigation: .default, fromMessage: MessageReference(message), fromReactionMessageId: hasReaction ? message.id : nil) - }) - }))) + ))) + } else { + items = ContextController.Items(content: .list([])) + } var packReferences: [StickerPackReference] = [] var existingIds = Set() @@ -315,6 +359,16 @@ extension ChatControllerImpl { } } + let reactionFile: TelegramMediaFile? + switch value { + case .builtin: + reactionFile = availableReactions?.reactions.first(where: { $0.value == value })?.selectAnimation + case let .custom(fileId): + reactionFile = customEmoji[fileId] + } + items.context = self.context + items.previewReaction = reactionFile + self.canReadHistory.set(false) let controller = ContextController(presentationData: self.presentationData, source: .extracted(ChatMessageReactionContextExtractedContentSource(chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, contentView: sourceView)), items: .single(items), recognizer: nil, gesture: gesture) diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift index d1445389c0e..3fa030cf7b8 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift @@ -31,7 +31,7 @@ func chatShareToSavedMessagesAdditionalView(_ chatController: ChatControllerImpl return } - let _ = (chatController.context.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: chatController.context.account.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 45, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: []) + let _ = (chatController.context.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: chatController.context.account.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: 45, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: []) |> map { view, _, _ -> [EngineMessage.Id] in let messageIds = correlationIds.compactMap { correlationId in return chatController.context.engine.messages.synchronouslyLookupCorrelationId(correlationId: correlationId) diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 623ebf78c81..4296bab4e62 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -231,7 +231,13 @@ func chatHistoryEntriesForView( } else { selection = .none } - entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }))) + + var isCentered = false + if case let .messageOptions(_, _, info) = associatedData.subject, case let .link(link) = info { + isCentered = link.isCentered + } + + entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: isCentered, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }))) } } else { let selection: ChatHistoryMessageSelection diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 7d611ad9016..991ab55f11f 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -98,6 +98,7 @@ struct ChatHistoryView { let id: Int32 let locationInput: ChatHistoryLocationInput? let ignoreMessagesInTimestampRange: ClosedRange? + let ignoreMessageIds: Set } enum ChatHistoryViewTransitionReason { @@ -220,7 +221,7 @@ extension ListMessageItemInteraction { }, openInstantPage: { message, data in controllerInteraction.openInstantPage(message, data) }, longTap: { action, message in - controllerInteraction.longTap(action, message) + controllerInteraction.longTap(action, ChatControllerInteraction.LongTapParams(message: message)) }, getHiddenMedia: { return controllerInteraction.hiddenMedia }) @@ -349,6 +350,7 @@ private func extractAssociatedData( chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, + preferredStoryHighQuality: Bool, animatedEmojiStickers: [String: [StickerPackItem]], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]], subject: ChatControllerSubject?, @@ -424,7 +426,7 @@ private func extractAssociatedData( automaticDownloadPeerId = message.peerId } - return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline) + return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline) } private extension ChatHistoryLocationInput { @@ -561,6 +563,15 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } + private let ignoreMessageIdsPromise = ValuePromise>(Set()) + var ignoreMessageIds: Set = Set() { + didSet { + if self.ignoreMessageIds != oldValue { + self.ignoreMessageIdsPromise.set(self.ignoreMessageIds) + } + } + } + private let chatHasBotsPromise = ValuePromise(false) var chatHasBots: Bool = false { didSet { @@ -1293,7 +1304,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto let currentViewVersion = self.currentViewVersion - let historyViewUpdate: Signal<(ChatHistoryViewUpdate, Int, ChatHistoryLocationInput?, ClosedRange?), NoError> + let historyViewUpdate: Signal<(ChatHistoryViewUpdate, Int, ChatHistoryLocationInput?, ClosedRange?, Set), NoError> var isFirstTime = true var updateAllOnEachVersion = false if case let .custom(messages, at, quote, _) = self.source { @@ -1316,12 +1327,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto scrollPosition = nil } - return (ChatHistoryViewUpdate.HistoryView(view: MessageHistoryView(tag: nil, namespaces: .all, entries: messages.reversed().map { MessageHistoryEntry(message: $0, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)) }, holeEarlier: hasMore, holeLater: false, isLoading: false), type: .Generic(type: version > 0 ? ViewUpdateType.Generic : ViewUpdateType.Initial), scrollPosition: scrollPosition, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: nil, buttonKeyboardMessage: nil, cachedData: nil, cachedDataMessages: nil, readStateData: nil), id: 0), version, nil, nil) + return (ChatHistoryViewUpdate.HistoryView(view: MessageHistoryView(tag: nil, namespaces: .all, entries: messages.reversed().map { MessageHistoryEntry(message: $0, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)) }, holeEarlier: hasMore, holeLater: false, isLoading: false), type: .Generic(type: version > 0 ? ViewUpdateType.Generic : ViewUpdateType.Initial), scrollPosition: scrollPosition, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: nil, buttonKeyboardMessage: nil, cachedData: nil, cachedDataMessages: nil, readStateData: nil), id: 0), version, nil, nil, Set()) } } else if case let .customView(historyView) = self.source { historyViewUpdate = combineLatest(queue: .mainQueue(), self.chatHistoryLocationPromise.get(), - self.ignoreMessagesInTimestampRangePromise.get() + self.ignoreMessagesInTimestampRangePromise.get(), + self.ignoreMessageIdsPromise.get() ) |> distinctUntilChanged(isEqual: { lhs, rhs in if lhs.0 != rhs.0 { @@ -1330,9 +1342,12 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if lhs.1 != rhs.1 { return false } + if lhs.2 != rhs.2 { + return false + } return true }) - |> mapToSignal { location, _ -> Signal<((MessageHistoryView, ViewUpdateType), ChatHistoryLocationInput?), NoError> in + |> mapToSignal { location, _, _ -> Signal<((MessageHistoryView, ViewUpdateType), ChatHistoryLocationInput?), NoError> in return historyView |> map { historyView in return (historyView, location) @@ -1377,13 +1392,15 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto ), version, location, - nil + nil, + Set() ) } } else { historyViewUpdate = combineLatest(queue: .mainQueue(), self.chatHistoryLocationPromise.get(), - self.ignoreMessagesInTimestampRangePromise.get() + self.ignoreMessagesInTimestampRangePromise.get(), + self.ignoreMessageIdsPromise.get() ) |> distinctUntilChanged(isEqual: { lhs, rhs in if lhs.0 != rhs.0 { @@ -1392,10 +1409,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if lhs.1 != rhs.1 { return false } + if lhs.2 != rhs.2 { + return false + } return true }) - |> mapToSignal { location, ignoreMessagesInTimestampRange in - return chatHistoryViewForLocation(location, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, context: context, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, scheduled: isScheduledMessages, fixedCombinedReadStates: fixedCombinedReadStates.with { $0 }, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, additionalData: additionalData, orderStatistics: [], useRootInterfaceStateForThread: useRootInterfaceStateForThread) + |> mapToSignal { location, ignoreMessagesInTimestampRange, ignoreMessageIds in + return chatHistoryViewForLocation(location, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, context: context, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, scheduled: isScheduledMessages, fixedCombinedReadStates: fixedCombinedReadStates.with { $0 }, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, additionalData: additionalData, orderStatistics: [], useRootInterfaceStateForThread: useRootInterfaceStateForThread) |> beforeNext { viewUpdate in switch viewUpdate { case let .HistoryView(view, _, _, _, _, _, _): @@ -1404,7 +1424,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto break } } - |> map { view -> (ChatHistoryViewUpdate, Int, ChatHistoryLocationInput?, ClosedRange?) in + |> map { view -> (ChatHistoryViewUpdate, Int, ChatHistoryLocationInput?, ClosedRange?, Set) in let version = currentViewVersion.modify({ value in if let value = value { return value + 1 @@ -1412,7 +1432,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto return 0 } })! - return (view, version, location, ignoreMessagesInTimestampRange) + return (view, version, location, ignoreMessagesInTimestampRange, ignoreMessageIds) } } } @@ -1599,6 +1619,22 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) + let preferredStoryHighQuality: Signal = combineLatest( + context.sharedContext.automaticMediaDownloadSettings + |> map { settings in + return settings.highQualityStories + } + |> distinctUntilChanged, + context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) + ) + ) + |> map { setting, peer -> Bool in + let isPremium = peer?.isPremium ?? false + return setting && isPremium + } + |> distinctUntilChanged + let messageViewQueue = Queue.mainQueue() let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue, historyViewUpdate, @@ -1606,6 +1642,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto selectedMessages, updatingMedia, automaticDownloadNetworkType, + preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, @@ -1624,7 +1661,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto audioTranscriptionTrial, chatThemes, deviceContactsNumbers - ).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers in + ).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers in let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots, allAdMessages) = promises func applyHole() { @@ -1705,7 +1742,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if resetScrolling, let previousViewValue = previousView.with({ $0 })?.0 { let filteredEntries: [ChatHistoryEntry] = [] - let processedView = ChatHistoryView(originalView: MessageHistoryView(tag: nil, namespaces: .all, entries: [], holeEarlier: false, holeLater: false, isLoading: true), filteredEntries: filteredEntries, associatedData: previousViewValue.associatedData, lastHeaderId: 0, id: previousViewValue.id, locationInput: previousViewValue.locationInput, ignoreMessagesInTimestampRange: nil) + let processedView = ChatHistoryView(originalView: MessageHistoryView(tag: nil, namespaces: .all, entries: [], holeEarlier: false, holeLater: false, isLoading: true), filteredEntries: filteredEntries, associatedData: previousViewValue.associatedData, lastHeaderId: 0, id: previousViewValue.id, locationInput: previousViewValue.locationInput, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set()) let previousValueAndVersion = previousView.swap((processedView, update.1, selectedMessages, allAdMessages.version)) let previous = previousValueAndVersion?.0 let previousSelectedMessages = previousValueAndVersion?.2 @@ -1844,7 +1881,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto translateToLanguage = languageCode } - let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated) + let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated) var includeEmbeddedSavedChatInfo = false if case let .replyThread(message) = chatLocation, message.peerId == context.account.peerId, !rotated { @@ -1878,7 +1915,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto dynamicAdMessages: allAdMessages.opportunistic ) let lastHeaderId = filteredEntries.last.flatMap { listMessageDateHeaderId(timestamp: $0.index.timestamp) } ?? 0 - let processedView = ChatHistoryView(originalView: view, filteredEntries: filteredEntries, associatedData: associatedData, lastHeaderId: lastHeaderId, id: id, locationInput: update.2, ignoreMessagesInTimestampRange: update.3) + let processedView = ChatHistoryView(originalView: view, filteredEntries: filteredEntries, associatedData: associatedData, lastHeaderId: lastHeaderId, id: id, locationInput: update.2, ignoreMessagesInTimestampRange: update.3, ignoreMessageIds: update.4) let previousValueAndVersion = previousView.swap((processedView, update.1, selectedMessages, allAdMessages.version)) let previous = previousValueAndVersion?.0 let previousSelectedMessages = previousValueAndVersion?.2 @@ -1908,6 +1945,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } else if let previous = previous, previous.id == processedView.id, previous.originalView.entries == processedView.originalView.entries { reason = ChatHistoryViewTransitionReason.InteractiveChanges updatedScrollPosition = nil + } else if let previous = previous, previous.id == processedView.id, previous.ignoreMessageIds != processedView.ignoreMessageIds { + reason = ChatHistoryViewTransitionReason.InteractiveChanges + updatedScrollPosition = nil } else { switch type { case let .Initial(fadeIn): @@ -2665,6 +2705,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if invoice.version != TelegramMediaInvoice.lastVersion { contentRequiredValidation = true } + } else if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first, case .preview = extendedMedia { + messageIdsWithInactiveExtendedMedia.insert(message.id) } else if let _ = media as? TelegramMediaStory { storiesRequiredValidation = true } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let _ = content.story { @@ -3455,6 +3497,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } } + for id in self.ignoreMessageIds { + inner: for (stableId, listId) in maybeRemovedInteractivelyMessageIds { + if listId == id { + expiredMessageStableIds.insert(stableId) + break inner + } + } + } } self.currentDeleteAnimationCorrelationIds.formUnion(expiredMessageStableIds) @@ -4157,7 +4207,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } else if self.interactiveReadActionDisposable == nil { if case let .peer(peerId) = self.chatLocation { - if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { + if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory && !self.context.account.isSupportUser { self.interactiveReadActionDisposable = self.context.engine.messages.installInteractiveReadMessagesAction(peerId: peerId) } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift index b1a88c7e935..5f78baacda6 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift @@ -18,7 +18,7 @@ func preloadedChatHistoryViewForLocation(_ location: ChatHistoryLocationInput, c tag = .tag(.pinned) } - return (chatHistoryViewForLocation(location, ignoreMessagesInTimestampRange: nil, context: context, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, scheduled: isScheduled, fixedCombinedReadStates: fixedCombinedReadStates, tag: tag, appendMessagesFromTheSameGroup: false, additionalData: additionalData, orderStatistics: orderStatistics) + return (chatHistoryViewForLocation(location, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), context: context, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, scheduled: isScheduled, fixedCombinedReadStates: fixedCombinedReadStates, tag: tag, appendMessagesFromTheSameGroup: false, additionalData: additionalData, orderStatistics: orderStatistics) |> castError(Bool.self) |> mapToSignal { update -> Signal in switch update { @@ -36,7 +36,21 @@ func preloadedChatHistoryViewForLocation(_ location: ChatHistoryLocationInput, c |> restartIfError } -func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMessagesInTimestampRange: ClosedRange?, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, scheduled: Bool, fixedCombinedReadStates: MessageHistoryViewReadState?, tag: HistoryViewInputTag?, appendMessagesFromTheSameGroup: Bool, additionalData: [AdditionalMessageHistoryViewData], orderStatistics: MessageHistoryViewOrderStatistics = [], useRootInterfaceStateForThread: Bool = false) -> Signal { +func chatHistoryViewForLocation( + _ location: ChatHistoryLocationInput, + ignoreMessagesInTimestampRange: ClosedRange?, + ignoreMessageIds: Set, + context: AccountContext, + chatLocation: ChatLocation, + chatLocationContextHolder: Atomic, + scheduled: Bool, + fixedCombinedReadStates: MessageHistoryViewReadState?, + tag: HistoryViewInputTag?, + appendMessagesFromTheSameGroup: Bool, + additionalData: [AdditionalMessageHistoryViewData], + orderStatistics: MessageHistoryViewOrderStatistics = [], + useRootInterfaceStateForThread: Bool = false +) -> Signal { let account = context.account if scheduled { var first = true @@ -93,9 +107,9 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess } if requestAroundId { - signal = account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, index: .upperBound, anchorIndex: .upperBound, count: count, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: preFixedReadState, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, useRootInterfaceStateForThread: useRootInterfaceStateForThread) + signal = account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, index: .upperBound, anchorIndex: .upperBound, count: count, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: preFixedReadState, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, useRootInterfaceStateForThread: useRootInterfaceStateForThread) } else { - signal = account.viewTracker.aroundMessageOfInterestHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData, useRootInterfaceStateForThread: useRootInterfaceStateForThread) + signal = account.viewTracker.aroundMessageOfInterestHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, count: count, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData, useRootInterfaceStateForThread: useRootInterfaceStateForThread) } let isPossibleIntroLoaded: Signal @@ -220,9 +234,9 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> switch searchLocationSubject.location { case let .index(index): - signal = account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, index: .message(index), anchorIndex: .message(index), count: count, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: nil, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData, useRootInterfaceStateForThread: useRootInterfaceStateForThread) + signal = account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, index: .message(index), anchorIndex: .message(index), count: count, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: nil, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData, useRootInterfaceStateForThread: useRootInterfaceStateForThread) case let .id(id): - signal = account.viewTracker.aroundIdMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, ignoreRelatedChats: ignoreRelatedChats, messageId: id, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData, useRootInterfaceStateForThread: useRootInterfaceStateForThread) + signal = account.viewTracker.aroundIdMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, count: count, ignoreRelatedChats: ignoreRelatedChats, messageId: id, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData, useRootInterfaceStateForThread: useRootInterfaceStateForThread) } return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in @@ -271,7 +285,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess } case let .Navigation(index, anchorIndex, count, _): var first = true - return account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, index: index, anchorIndex: anchorIndex, count: count, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData, useRootInterfaceStateForThread: useRootInterfaceStateForThread) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in + return account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, index: index, anchorIndex: anchorIndex, count: count, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData, useRootInterfaceStateForThread: useRootInterfaceStateForThread) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in let (cachedData, cachedDataMessages, readStateData) = extractAdditionalData(view: view, chatLocation: chatLocation) let genericType: ViewUpdateType @@ -287,7 +301,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > subject.index ? .Down : .Up let chatScrollPosition = ChatHistoryViewScrollPosition.index(subject: subject, position: scrollPosition, directionHint: directionHint, animated: animated, highlight: highlight, displayLink: false) var first = true - return account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, index: subject.index, anchorIndex: anchorIndex, count: 128, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData, useRootInterfaceStateForThread: useRootInterfaceStateForThread) + return account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, index: subject.index, anchorIndex: anchorIndex, count: 128, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData, useRootInterfaceStateForThread: useRootInterfaceStateForThread) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in let (cachedData, cachedDataMessages, readStateData) = extractAdditionalData(view: view, chatLocation: chatLocation) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index f8a1129b87f..29f29f6d384 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -304,7 +304,7 @@ private func canViewReadStats(message: Message, participantCount: Int?, isMessag func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, accountPeerId: PeerId) -> Bool { if case let .customChatContents(contents) = chatPresentationInterfaceState.subject, case .hashTagSearch = contents.kind { - return false + return true } if case .customChatContents = chatPresentationInterfaceState.chatLocation { return true @@ -325,7 +325,7 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS } switch chatPresentationInterfaceState.mode { case .inline: - return false + return true case .standard(.embedded): return false default: @@ -927,15 +927,17 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState messageActions.editTags = Set() } - return (MessageContextMenuData( + let data = MessageContextMenuData( starStatus: stickerSaveStatus, - canReply: canReply && !isEmbeddedMode, + canReply: canReply, canPin: canPin && !isEmbeddedMode, canEdit: canEdit && !isEmbeddedMode, canSelect: canSelect && !isEmbeddedMode, resourceStatus: resourceStatus, messageActions: messageActions - ), updatingMessageMedia, infoSummaryData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions, translationSettings, loggingSettings, notificationSoundList, accountPeer) + ) + + return (data, updatingMessageMedia, infoSummaryData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions, translationSettings, loggingSettings, notificationSoundList, accountPeer) } return dataSignal @@ -1790,7 +1792,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState clearCacheAsDelete = true } - if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, canEditFactCheck(appConfig: appConfig) { + if message.id.namespace == Namespaces.Message.Cloud, let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, canEditFactCheck(appConfig: appConfig) { var canAddFactCheck = true if message.media.contains(where: { $0 is TelegramMediaAction || $0 is TelegramMediaGiveaway }) { canAddFactCheck = false @@ -2500,6 +2502,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer var disableDelete = false var isCopyProtected = false var isShareProtected = false + var isExternalShareProtected = false var setTag = false var commonTags: Set? @@ -2556,6 +2559,8 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer for media in message.media { if let invoice = media as? TelegramMediaInvoice, let _ = invoice.extendedMedia { isShareProtected = true + } else if let _ = media as? TelegramMediaPaidContent { + isExternalShareProtected = true } else if let file = media as? TelegramMediaFile, file.isSticker { for case let .Sticker(_, packReference, _) in file.attributes { if let _ = packReference { @@ -2758,7 +2763,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer } } - if !isShareProtected { + if !isShareProtected && !isExternalShareProtected { optionsMap[id]!.insert(.externalShare) } } diff --git a/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift index ca679407998..2a91e8555c7 100644 --- a/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift @@ -90,7 +90,7 @@ private final class ChatManagingBotTitlePanelComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ChatManagingBotTitlePanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: ChatManagingBotTitlePanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let topInset: CGFloat = 6.0 @@ -243,7 +243,7 @@ private final class ChatManagingBotTitlePanelComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -396,7 +396,7 @@ final class ChatManagingBotTitlePanelNode: ChatTitleAccessoryPanelNode { if let managingBot = interfaceState.contactStatus?.managingBot { let contentSize = self.content.update( - transition: Transition(transition), + transition: ComponentTransition(transition), component: AnyComponent(ChatManagingBotTitlePanelComponent( context: self.context, theme: interfaceState.theme, diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift index d6630732859..20419a65a77 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -243,6 +243,8 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran final class DecorationItemNodeImpl: ASDisplayNode, ChatMessageTransitionNode.DecorationItemNode { let itemNode: ChatMessageItemNodeProtocol let contentView: UIView + var globalPortalSourceView: PortalSourceView? + let aboveEverything: Bool private let getContentAreaInScreenSpace: () -> CGRect private let scrollingContainer: ASDisplayNode @@ -251,9 +253,10 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran fileprivate weak var overlayController: OverlayTransitionContainerController? - init(itemNode: ChatMessageItemNodeProtocol, contentView: UIView, getContentAreaInScreenSpace: @escaping () -> CGRect) { + init(itemNode: ChatMessageItemNodeProtocol, contentView: UIView, aboveEverything: Bool, getContentAreaInScreenSpace: @escaping () -> CGRect) { self.itemNode = itemNode self.contentView = contentView + self.aboveEverything = aboveEverything self.getContentAreaInScreenSpace = getContentAreaInScreenSpace self.clippingNode = ASDisplayNode() @@ -267,7 +270,16 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran self.addSubnode(self.clippingNode) self.clippingNode.addSubnode(self.scrollingContainer) self.scrollingContainer.addSubnode(self.containerNode) - self.containerNode.view.addSubview(self.contentView) + + if aboveEverything { + let globalPortalSourceView = PortalSourceView() + globalPortalSourceView.needsGlobalPortal = true + self.globalPortalSourceView = globalPortalSourceView + globalPortalSourceView.addSubview(self.contentView) + self.containerNode.view.addSubview(globalPortalSourceView) + } else { + self.containerNode.view.addSubview(self.contentView) + } } func updateLayout(size: CGSize) { @@ -275,6 +287,9 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran let absoluteRect = self.itemNode.view.convert(self.itemNode.view.bounds, to: self.itemNode.supernode?.supernode?.view) self.containerNode.frame = absoluteRect + if let globalPortalSourceView = self.globalPortalSourceView { + globalPortalSourceView.frame = CGRect(origin: CGPoint(), size: size) + } } func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { @@ -962,8 +977,8 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran self.listNode.setCurrentSendAnimationCorrelationIds(correlationIds) } - public func add(decorationView: UIView, itemNode: ChatMessageItemNodeProtocol) -> DecorationItemNode { - let decorationItemNode = DecorationItemNodeImpl(itemNode: itemNode, contentView: decorationView, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace) + public func add(decorationView: UIView, itemNode: ChatMessageItemNodeProtocol, aboveEverything: Bool) -> DecorationItemNode { + let decorationItemNode = DecorationItemNodeImpl(itemNode: itemNode, contentView: decorationView, aboveEverything: aboveEverything, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace) decorationItemNode.updateLayout(size: self.bounds.size) self.decorationItemNodes.append(decorationItemNode) diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 9844a5678f8..9163d271b52 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -322,10 +322,15 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { messageUpdated = true } + var isStarsPayment = false if let message = interfaceState.pinnedMessage, !message.message.isRestricted(platform: "ios", contentSettings: self.context.currentContentSettings.with { $0 }) { for attribute in message.message.attributes { if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), attribute.rows.count == 1, attribute.rows[0].buttons.count == 1 { - actionTitle = attribute.rows[0].buttons[0].title + let title = attribute.rows[0].buttons[0].title + actionTitle = title + if case .payment = attribute.rows[0].buttons[0].action, title.contains("⭐️") { + isStarsPayment = true + } } } } else { @@ -430,7 +435,20 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.actionButtonBackgroundNode.isHidden = false self.actionButtonTitleNode.isHidden = false - self.actionButtonTitleNode.attributedText = NSAttributedString(string: actionTitle, font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: interfaceState.theme.list.itemCheckColors.foregroundColor) + let attributedTitle: NSAttributedString + if isStarsPayment { + let updatedTitle = actionTitle.replacingOccurrences(of: "⭐️", with: " # ") + let buttonAttributedString = NSMutableAttributedString(string: updatedTitle, font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: interfaceState.theme.list.itemCheckColors.foregroundColor) + if let range = buttonAttributedString.string.range(of: "#"), let starImage = UIImage(bundleImageName: "Item List/PremiumIcon") { + buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.foregroundColor, value: interfaceState.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string)) + } + attributedTitle = buttonAttributedString + } else { + attributedTitle = NSAttributedString(string: actionTitle, font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: interfaceState.theme.list.itemCheckColors.foregroundColor) + } + self.actionButtonTitleNode.attributedText = attributedTitle let actionButtonTitleSize = self.actionButtonTitleNode.updateLayout(CGSize(width: 150.0, height: .greatestFiniteMagnitude)) let actionButtonSize = CGSize(width: max(actionButtonTitleSize.width + 20.0, 40.0), height: 28.0) @@ -620,6 +638,28 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { imageDimensions = dimensions.cgSize } break + } else if let paidContent = media as? TelegramMediaPaidContent, let firstMedia = paidContent.extendedMedia.first { + switch firstMedia { + case let .preview(dimensions, immediateThumbnailData, _): + let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) + if let dimensions { + imageDimensions = dimensions.cgSize + } + updatedMediaReference = .standalone(media: thumbnailMedia) + case let .full(fullMedia): + updatedMediaReference = .message(message: MessageReference(message), media: fullMedia) + if let image = fullMedia as? TelegramMediaImage { + if let representation = largestRepresentationForPhoto(image) { + imageDimensions = representation.dimensions.cgSize + } + break + } else if let file = fullMedia as? TelegramMediaFile { + if let dimensions = file.dimensions { + imageDimensions = dimensions.cgSize + } + break + } + } } } } @@ -663,7 +703,11 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { if mediaUpdated { if let updatedMediaReference = updatedMediaReference, imageDimensions != nil { if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { - updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, blurred: hasSpoiler) + if imageReference.media.representations.isEmpty { + updateImageSignal = chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, ignoreFullSize: true, synchronousLoad: true) + } else { + updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, blurred: hasSpoiler) + } } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { if fileReference.media.isAnimatedSticker { let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512) @@ -911,7 +955,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { controllerInteraction.activateSwitchInline(peerId, "@\(addressName) \(query)", peerTypes) } case .payment: - controllerInteraction.openCheckoutOrReceipt(message.id) + controllerInteraction.openCheckoutOrReceipt(message.id, nil) case let .urlAuth(url, buttonId): controllerInteraction.requestMessageActionUrlAuth(url, .message(id: message.id, buttonId: buttonId)) case .setupPoll: diff --git a/submodules/TelegramUI/Sources/ChatPremiumRequiredInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatPremiumRequiredInputPanelNode.swift index de6fe788e49..0add3d1da05 100644 --- a/submodules/TelegramUI/Sources/ChatPremiumRequiredInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPremiumRequiredInputPanelNode.swift @@ -78,13 +78,13 @@ final class ChatPremiumRequiredInputPanelNode: ChatInputPanelNode { return currentLayout.height } - let height = self.update(params: params, transition: Transition(transition)) + let height = self.update(params: params, transition: ComponentTransition(transition)) self.currentLayout = Layout(params: params, height: height) return height } - private func update(params: Params, transition: Transition) -> CGFloat { + private func update(params: Params, transition: ComponentTransition) -> CGFloat { let height: CGFloat if case .regular = params.metrics.widthClass { height = 49.0 diff --git a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift index 6f11af4fe48..23f8124bfce 100644 --- a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift @@ -20,8 +20,14 @@ import TelegramNotices import ComponentFlow import MediaScrubberComponent +//Xcode 16 +#if canImport(ContactProvider) +extension AudioWaveformNode: @retroactive CustomMediaPlayerScrubbingForegroundNode { +} +#else extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode { } +#endif final class ChatRecordingPreviewViewForOverlayContent: UIView, ChatInputPanelViewForOverlayContent { let ignoreHit: (UIView, CGPoint) -> Bool diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 06d3d750282..add22dd404e 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -267,7 +267,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, ASScrollViewDe switch item.content { case let .peer(peerData): if let message = peerData.messages.first { - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerData.peer.peerId), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), botStart: nil, mode: .standard(.previewing)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerData.peer.peerId), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(content: .list([]))), gesture: gesture) presentInGlobalOverlay(contextController) diff --git a/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift index 3b9af15fca8..1dbeb89377e 100644 --- a/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift @@ -106,7 +106,7 @@ final class ChatSearchTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, Chat if highlighted { self.containerButton.alpha = 0.7 } else { - Transition.easeInOut(duration: 0.25).setAlpha(view: self.containerButton, alpha: 1.0) + ComponentTransition.easeInOut(duration: 0.25).setAlpha(view: self.containerButton, alpha: 1.0) } } } @@ -119,7 +119,7 @@ final class ChatSearchTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, Chat self.action() } - func update(theme: PresentationTheme, strings: PresentationStrings, height: CGFloat, isUnlock: Bool, transition: Transition) -> CGSize { + func update(theme: PresentationTheme, strings: PresentationStrings, height: CGFloat, isUnlock: Bool, transition: ComponentTransition) -> CGSize { let titleIconSpacing: CGFloat = 0.0 let titleIconSize = self.titleIcon.update( @@ -308,7 +308,7 @@ final class ChatSearchTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, Chat return super.hitTest(mappedPoint, with: event) } - func update(item: Item, isSelected: Bool, isLocked: Bool, theme: PresentationTheme, height: CGFloat, transition: Transition) -> CGSize { + func update(item: Item, isSelected: Bool, isLocked: Bool, theme: PresentationTheme, height: CGFloat, transition: ComponentTransition) -> CGSize { let spacing: CGFloat = 3.0 let contentsAlpha: CGFloat = isLocked ? 0.6 : 1.0 diff --git a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift index 0e5eaa3c476..8a2fbb07578 100644 --- a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift @@ -122,7 +122,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { return currentLayout.height } - let height = self.update(params: params, transition: Transition(transition)) + let height = self.update(params: params, transition: ComponentTransition(transition)) self.currentLayout = Layout(params: params, height: height) return height @@ -133,7 +133,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { self.tagMessageCount = (tag, count, nil) } - private func update(transition: Transition) { + private func update(transition: ComponentTransition) { if self.isUpdating { return } @@ -142,7 +142,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { } } - private func update(params: Params, transition: Transition) -> CGFloat { + private func update(params: Params, transition: ComponentTransition) -> CGFloat { self.isUpdating = true defer { self.isUpdating = false @@ -455,10 +455,20 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { let resultsTextSize = resultsText.update( transition: resultsTextTransition, - component: AnyComponent(AnimatedTextComponent( - font: Font.regular(15.0), - color: params.interfaceState.theme.rootController.navigationBar.secondaryTextColor, - items: resultsTextString + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(AnimatedTextComponent( + font: Font.regular(15.0), + color: (params.interfaceState.displayHistoryFilterAsList && canChangeListMode) ? params.interfaceState.theme.rootController.navigationBar.accentTextColor : params.interfaceState.theme.rootController.navigationBar.secondaryTextColor, + items: resultsTextString + )), + effectAlignment: .center, + action: { [weak self] in + guard let self, let params = self.currentLayout?.params else { + return + } + self.interfaceInteraction?.updateDisplayHistoryFilterAsList(!params.interfaceState.displayHistoryFilterAsList) + }, + isEnabled: params.interfaceState.displayHistoryFilterAsList && canChangeListMode )), environment: {}, containerSize: CGSize(width: 200.0, height: 100.0) diff --git a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift index 09c63ace837..318de27a4de 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift @@ -12,8 +12,121 @@ import ChatControllerInteraction import AccountContext import ChatTextInputMediaRecordingButton import ChatSendButtonRadialStatusNode +import ChatSendMessageActionUI +import ComponentFlow -final class ChatTextInputActionButtonsNode: ASDisplayNode { +private final class EffectBadgeView: UIView { + private let context: AccountContext + private var currentEffectId: Int64? + + private let backgroundView: UIImageView + + private var theme: PresentationTheme? + + private var effect: AvailableMessageEffects.MessageEffect? + private var effectIcon: ComponentView? + + private let effectDisposable = MetaDisposable() + + init(context: AccountContext) { + self.context = context + self.backgroundView = UIImageView() + + super.init(frame: CGRect()) + + self.isUserInteractionEnabled = false + + self.addSubview(self.backgroundView) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + deinit { + self.effectDisposable.dispose() + } + + func update(size: CGSize, theme: PresentationTheme, effectId: Int64) { + if self.theme !== theme { + self.theme = theme + self.backgroundView.image = generateFilledCircleImage(diameter: size.width, color: theme.list.plainBackgroundColor, strokeColor: nil, strokeWidth: nil, backgroundColor: nil) + self.backgroundView.layer.shadowPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: size)).cgPath + self.backgroundView.layer.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor + self.backgroundView.layer.shadowOpacity = 0.14 + self.backgroundView.layer.shadowOffset = CGSize(width: 0.0, height: 1.0) + self.backgroundView.layer.shadowRadius = 1.0 + } + + self.backgroundView.frame = CGRect(origin: CGPoint(), size: size) + + if self.currentEffectId != effectId { + self.currentEffectId = effectId + + let messageEffect = self.context.engine.stickers.availableMessageEffects() + |> take(1) + |> map { availableMessageEffects -> AvailableMessageEffects.MessageEffect? in + guard let availableMessageEffects else { + return nil + } + for messageEffect in availableMessageEffects.messageEffects { + if messageEffect.id == effectId || messageEffect.effectSticker.fileId.id == effectId { + return messageEffect + } + } + return nil + } + + self.effectDisposable.set((messageEffect |> deliverOnMainQueue).start(next: { [weak self] effect in + guard let self, let effect else { + return + } + self.effect = effect + self.updateIcon() + })) + } + } + + private func updateIcon() { + guard let effect else { + return + } + + let effectIcon: ComponentView + if let current = self.effectIcon { + effectIcon = current + } else { + effectIcon = ComponentView() + self.effectIcon = effectIcon + } + let effectIconContent: ChatSendMessageScreenEffectIcon.Content + if let staticIcon = effect.staticIcon { + effectIconContent = .file(staticIcon) + } else { + effectIconContent = .text(effect.emoticon) + } + let effectIconSize = effectIcon.update( + transition: .immediate, + component: AnyComponent(ChatSendMessageScreenEffectIcon( + context: self.context, + content: effectIconContent + )), + environment: {}, + containerSize: CGSize(width: 8.0, height: 8.0) + ) + + let size = CGSize(width: 16.0, height: 16.0) + if let effectIconView = effectIcon.view { + if effectIconView.superview == nil { + self.addSubview(effectIconView) + } + effectIconView.frame = CGRect(origin: CGPoint(x: floor((size.width - effectIconSize.width) * 0.5), y: floor((size.height - effectIconSize.height) * 0.5)), size: effectIconSize) + } + } +} + +final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageActionSheetControllerSourceSendButtonNode { + private let context: AccountContext private let presentationContext: ChatPresentationContext? private let strings: PresentationStrings @@ -26,6 +139,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode { var sendButtonHasApplyIcon = false var animatingSendButton = false let expandMediaInputButton: HighlightableButtonNode + private var effectBadgeView: EffectBadgeView? var sendButtonLongPressed: ((ASDisplayNode, ContextGesture) -> Void)? @@ -42,6 +156,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode { private var validLayout: CGSize? init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { + self.context = context self.presentationContext = presentationContext let theme = presentationInterfaceState.theme let strings = presentationInterfaceState.strings @@ -145,7 +260,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode { } } - func updateLayout(size: CGSize, isMediaInputExpanded: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + func updateLayout(size: CGSize, isMediaInputExpanded: Bool, currentMessageEffectId: Int64?, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { self.validLayout = size transition.updateFrame(layer: self.micButton.layer, frame: CGRect(origin: CGPoint(), size: size)) self.micButton.layoutItems() @@ -154,7 +269,8 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode { transition.updateFrame(node: self.sendContainerNode, frame: CGRect(origin: CGPoint(), size: size)) let backgroundSize = CGSize(width: 33.0, height: 33.0) - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - backgroundSize.width) / 2.0), y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize)) + let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - backgroundSize.width) / 2.0), y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) self.backgroundNode.cornerRadius = backgroundSize.width / 2.0 transition.updateFrame(node: self.backdropNode, frame: CGRect(origin: CGPoint(x: -2.0, y: -2.0), size: CGSize(width: size.width + 12.0, height: size.height + 2.0))) @@ -162,42 +278,31 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode { self.backdropNode.update(rect: rect, within: containerSize) } -// var isScheduledMessages = false -// if case .scheduledMessages = interfaceState.subject { -// isScheduledMessages = true -// } -// -// if let slowmodeState = interfaceState.slowmodeState, !isScheduledMessages && interfaceState.editMessageState == nil { -// let sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode -// if let current = self.sendButtonRadialStatusNode { -// sendButtonRadialStatusNode = current -// } else { -// sendButtonRadialStatusNode = ChatSendButtonRadialStatusNode(color: interfaceState.theme.chat.inputPanel.panelControlAccentColor) -// sendButtonRadialStatusNode.alpha = self.sendContainerNode.alpha -// self.sendButtonRadialStatusNode = sendButtonRadialStatusNode -// self.addSubnode(sendButtonRadialStatusNode) -// } -// -// transition.updateSublayerTransformScale(layer: self.sendContainerNode.layer, scale: CGPoint(x: 0.7575, y: 0.7575)) -// -// let defaultSendButtonSize: CGFloat = 25.0 -// let defaultOriginX = floorToScreenPixels((self.sendButton.bounds.width - defaultSendButtonSize) / 2.0) -// let defaultOriginY = floorToScreenPixels((self.sendButton.bounds.height - defaultSendButtonSize) / 2.0) -// -// let radialStatusFrame = CGRect(origin: CGPoint(x: defaultOriginX - 4.0, y: defaultOriginY - 4.0), size: CGSize(width: 33.0, height: 33.0)) -// sendButtonRadialStatusNode.frame = radialStatusFrame -// sendButtonRadialStatusNode.slowmodeState = slowmodeState -// } else { -// if let sendButtonRadialStatusNode = self.sendButtonRadialStatusNode { -// self.sendButtonRadialStatusNode = nil -// sendButtonRadialStatusNode.removeFromSupernode() -// } -// transition.updateSublayerTransformScale(layer: self.sendContainerNode.layer, scale: CGPoint(x: 1.0, y: 1.0)) -// } - transition.updateFrame(node: self.expandMediaInputButton, frame: CGRect(origin: CGPoint(), size: size)) let expanded = isMediaInputExpanded transition.updateSublayerTransformScale(node: self.expandMediaInputButton, scale: CGPoint(x: 1.0, y: expanded ? 1.0 : -1.0)) + + if let currentMessageEffectId { + let effectBadgeView: EffectBadgeView + if let current = self.effectBadgeView { + effectBadgeView = current + } else { + effectBadgeView = EffectBadgeView(context: self.context) + self.effectBadgeView = effectBadgeView + self.sendContainerNode.view.addSubview(effectBadgeView) + + effectBadgeView.alpha = 0.0 + transition.updateAlpha(layer: effectBadgeView.layer, alpha: 1.0) + } + let badgeSize = CGSize(width: 16.0, height: 16.0) + effectBadgeView.frame = CGRect(origin: CGPoint(x: backgroundFrame.minX + backgroundSize.width + 3.0 - badgeSize.width, y: backgroundFrame.minY + backgroundSize.height + 3.0 - badgeSize.height), size: badgeSize) + effectBadgeView.update(size: badgeSize, theme: interfaceState.theme, effectId: currentMessageEffectId) + } else if let effectBadgeView = self.effectBadgeView { + self.effectBadgeView = nil + transition.updateAlpha(layer: effectBadgeView.layer, alpha: 0.0, completion: { [weak effectBadgeView] _ in + effectBadgeView?.removeFromSuperview() + }) + } } func updateAccessibility() { @@ -216,4 +321,17 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode { self.accessibilityHint = nil } } + + func makeCustomContents() -> UIView? { + if self.sendButtonHasApplyIcon || self.effectBadgeView != nil { + let result = UIView() + result.frame = self.bounds + if let copyView = self.sendContainerNode.view.snapshotView(afterScreenUpdates: false) { + copyView.frame = self.sendContainerNode.frame + result.addSubview(copyView) + } + return result + } + return nil + } } diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 5b1bafba999..8c8d1bd54cd 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -1786,7 +1786,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.menuButtonIconNode.enqueueState(.close, animated: false) } else if case .webView = interfaceState.botMenuButton, let previousShowWebView = previousState?.showWebView, previousShowWebView != interfaceState.showWebView { if interfaceState.showWebView { - self.menuButtonIconNode.enqueueState(.close, animated: true) +// self.menuButtonIconNode.enqueueState(.close, animated: true) } else { self.menuButtonIconNode.enqueueState(.app, animated: true) } @@ -2533,7 +2533,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } if let presentationInterfaceState = self.presentationInterfaceState { - self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), isMediaInputExpanded: isMediaInputExpanded, transition: transition, interfaceState: presentationInterfaceState) + self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), isMediaInputExpanded: isMediaInputExpanded, currentMessageEffectId: presentationInterfaceState.interfaceState.sendMessageEffect, transition: transition, interfaceState: presentationInterfaceState) } let slowModeButtonFrame = CGRect(origin: CGPoint(x: hideOffset.x + width - rightInset - 5.0 - slowModeButtonSize.width + composeButtonsOffset, y: hideOffset.y + panelHeight - minimalHeight + 6.0), size: slowModeButtonSize) @@ -2605,7 +2605,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch //transition.updateFrame(node: textInputNode, frame: textFieldFrame) textInputNode.frame = textFieldFrame textInputNode.updateLayout(size: textFieldFrame.size) - self.updateInputField(textInputFrame: textFieldFrame, transition: Transition(transition)) + self.updateInputField(textInputFrame: textFieldFrame, transition: ComponentTransition(transition)) if shouldUpdateLayout { textInputNode.layout() } @@ -3243,7 +3243,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch private var dismissedEmojiSuggestionPosition: EmojiSuggestionPosition? - private func updateInputField(textInputFrame: CGRect, transition: Transition) { + private func updateInputField(textInputFrame: CGRect, transition: ComponentTransition) { guard let textInputNode = self.textInputNode, let context = self.context else { return } @@ -4593,7 +4593,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } else if case let .webView(title, url) = presentationInterfaceState.botMenuButton { let willShow = !(self.presentationInterfaceState?.showWebView ?? false) - if willShow { + if willShow || "".isEmpty { self.interfaceInteraction?.openWebView(title, url, false, .menu) } else { self.interfaceInteraction?.updateShowWebView { _ in diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index 6c309a01698..88d69874baa 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -275,7 +275,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { private let listView: ListView private let listBackgroundView: UIView private var currentEntries: [CommandChatInputContextPanelEntry]? - private var contentOffsetChangeTransition: Transition? + private var contentOffsetChangeTransition: ComponentTransition? private var isAnimatingOut: Bool = false private var enqueuedTransitions: [(CommandChatInputContextPanelTransition, Bool)] = [] @@ -323,7 +323,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { } } - let transition: Transition = self.contentOffsetChangeTransition ?? .immediate + let transition: ComponentTransition = self.contentOffsetChangeTransition ?? .immediate transition.setFrame(view: self.listBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: topItemOffset), size: CGSize(width: self.listView.bounds.width, height: self.listView.bounds.height + 1000.0))) } } @@ -503,7 +503,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve) - self.contentOffsetChangeTransition = Transition(transition) + self.contentOffsetChangeTransition = ComponentTransition(transition) self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) diff --git a/submodules/TelegramUI/Sources/CreateChannelController.swift b/submodules/TelegramUI/Sources/CreateChannelController.swift index c5f11b60484..d7cdbd02da4 100644 --- a/submodules/TelegramUI/Sources/CreateChannelController.swift +++ b/submodules/TelegramUI/Sources/CreateChannelController.swift @@ -19,6 +19,7 @@ import MapResourceToAvatarSizes import LegacyMediaPickerUI import TextFormat import AvatarEditorScreen +import OldChannelsController private struct CreateChannelArguments { let context: AccountContext diff --git a/submodules/TelegramUI/Sources/CreateGroupController.swift b/submodules/TelegramUI/Sources/CreateGroupController.swift index d1108c444da..3532b0b8271 100644 --- a/submodules/TelegramUI/Sources/CreateGroupController.swift +++ b/submodules/TelegramUI/Sources/CreateGroupController.swift @@ -32,6 +32,7 @@ import AsyncDisplayKit import TextFormat import AvatarEditorScreen import SendInviteLinkScreen +import OldChannelsController private struct CreateGroupArguments { let context: AccountContext diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index f99072a72a7..452a8deebfe 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -165,7 +165,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam controller.presentAttachmentBot(botId: attachBotStart.botId, payload: attachBotStart.payload, justInstalled: attachBotStart.justInstalled) } if let botAppStart = params.botAppStart, case let .peer(peer) = params.chatLocation { - controller.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload) + controller.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, compact: botAppStart.compact) } params.setupController(controller) found = true @@ -188,7 +188,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam } if let botAppStart = params.botAppStart, case let .peer(peer) = params.chatLocation { Queue.mainQueue().after(0.1) { - controller.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload) + controller.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, compact: botAppStart.compact) } } } else { @@ -196,7 +196,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam if let botAppStart = params.botAppStart, case let .peer(peer) = params.chatLocation { Queue.mainQueue().after(0.1) { - controller.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload) + controller.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, compact: botAppStart.compact) } } } diff --git a/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift b/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift index 79e16163fa0..e0ec73b4e9c 100644 --- a/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift +++ b/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift @@ -235,9 +235,12 @@ private extension NGDeeplinkHandler { @available(iOS 13.0, *) func handleSpecialOffer(url: URL) -> Bool { - return SpecialOfferTgHelper.showSpecialOfferFromDeeplink( - id: url.queryItems["id"] - ) + Task { @MainActor in + SpecialOfferTgHelper.showSpecialOfferFromDeeplink( + id: url.queryItems["id"] + ) + } + return true } func handlePstAuth(url: URL) -> Bool { diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index cbb4c970176..30f7ee7b64b 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -127,7 +127,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { return true } - if let mediaData = chatMessageGalleryControllerData(context: params.context, chatLocation: params.chatLocation, chatFilterTag: params.chatFilterTag, chatLocationContextHolder: params.chatLocationContextHolder, message: params.message, navigationController: params.navigationController, standalone: params.standalone, reverseMessageGalleryOrder: params.reverseMessageGalleryOrder, mode: params.mode, source: params.gallerySource, synchronousLoad: false, actionInteraction: params.actionInteraction) { + if let mediaData = chatMessageGalleryControllerData(context: params.context, chatLocation: params.chatLocation, chatFilterTag: params.chatFilterTag, chatLocationContextHolder: params.chatLocationContextHolder, message: params.message, mediaIndex: params.mediaIndex, navigationController: params.navigationController, standalone: params.standalone, reverseMessageGalleryOrder: params.reverseMessageGalleryOrder, mode: params.mode, source: params.gallerySource, synchronousLoad: false, actionInteraction: params.actionInteraction) { switch mediaData { case let .url(url): params.openUrl(url) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 3395f95322a..9bf678e4bf9 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -837,7 +837,7 @@ func openResolvedUrlImpl( } } let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { _ in - let controller = context.sharedContext.makeStarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: .slug(slug), inputData: starsInputData, completion: { _ in }) + let controller = context.sharedContext.makeStarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: .slug(slug), extendedMedia: [], inputData: starsInputData, completion: { _ in }) navigationController.pushViewController(controller) }) } else { @@ -1072,7 +1072,7 @@ func openResolvedUrlImpl( }, openMessage: { messageId in let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)) - |> deliverOnMainQueue).startStandalone(next: { peer in + |> deliverOnMainQueue).startStandalone(next: { peer in guard let peer else { return } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 7805c572b8b..e70e0bc209f 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -172,6 +172,11 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur return } + let walletDeeplinksManager = NicegramWallet.DeeplinksModule.shared.deeplinksManager() + if walletDeeplinksManager.handle(url) { + return + } + let nicegramHandler = NGDeeplinkHandler( tgAccountContext: context, navigationController: navigationController @@ -179,11 +184,6 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur if nicegramHandler.handle(url: url) { return } - - let walletDeeplinksManager = NicegramWallet.DeeplinksModule.shared.deeplinksManager() - if walletDeeplinksManager.handle(url) { - return - } // if forceExternal || url.lowercased().hasPrefix("tel:") || url.lowercased().hasPrefix("calshow:") { diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 116f1af799f..e810e8711c8 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -118,7 +118,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { _, _ in - }, openCheckoutOrReceipt: { _ in + }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in @@ -175,7 +175,6 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in }, openStickerEditor: { - }, openPhoneContextMenu: { _ in }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in diff --git a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift index 6830e72ed31..a09f5b61dec 100644 --- a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift +++ b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift @@ -566,7 +566,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { return .single(nil) } - return self.context.account.postbox.aroundMessageHistoryViewForLocation(self.context.chatLocationInput(for: chatLocation, contextHolder: self.chatLocationContextHolder ?? Atomic(value: nil)), anchor: .index(message.index), ignoreMessagesInTimestampRange: nil, count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: .tag(tagMask), appendMessagesFromTheSameGroup: false, namespaces: namespaces, orderStatistics: []) + return self.context.account.postbox.aroundMessageHistoryViewForLocation(self.context.chatLocationInput(for: chatLocation, contextHolder: self.chatLocationContextHolder ?? Atomic(value: nil)), anchor: .index(message.index), ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: .tag(tagMask), appendMessagesFromTheSameGroup: false, namespaces: namespaces, orderStatistics: []) |> mapToSignal { view -> Signal<(Message, [Message])?, NoError> in if let (message, aroundMessages, _) = navigatedMessageFromView(view.0, anchorIndex: message.index, position: .exact, reversed: reversed) { return .single((message, aroundMessages)) @@ -678,7 +678,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { guard let inputIndex = inputIndex else { return .single(nil) } - return self.context.account.postbox.aroundMessageHistoryViewForLocation(self.context.chatLocationInput(for: chatLocation, contextHolder: self.chatLocationContextHolder ?? Atomic(value: nil)), anchor: .index(inputIndex), ignoreMessagesInTimestampRange: nil, count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: .tag(tagMask), appendMessagesFromTheSameGroup: false, namespaces: namespaces, orderStatistics: []) + return self.context.account.postbox.aroundMessageHistoryViewForLocation(self.context.chatLocationInput(for: chatLocation, contextHolder: self.chatLocationContextHolder ?? Atomic(value: nil)), anchor: .index(inputIndex), ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: .tag(tagMask), appendMessagesFromTheSameGroup: false, namespaces: namespaces, orderStatistics: []) |> mapToSignal { view -> Signal<(Message, [Message])?, NoError> in let position: NavigatedMessageFromViewPosition switch navigation { @@ -708,7 +708,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } else { viewIndex = .lowerBound } - return self.context.account.postbox.aroundMessageHistoryViewForLocation(self.context.chatLocationInput(for: chatLocation, contextHolder: self.chatLocationContextHolder ?? Atomic(value: nil)), anchor: viewIndex, ignoreMessagesInTimestampRange: nil, count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: .tag(tagMask), appendMessagesFromTheSameGroup: false, namespaces: namespaces, orderStatistics: []) + return self.context.account.postbox.aroundMessageHistoryViewForLocation(self.context.chatLocationInput(for: chatLocation, contextHolder: self.chatLocationContextHolder ?? Atomic(value: nil)), anchor: viewIndex, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: .tag(tagMask), appendMessagesFromTheSameGroup: false, namespaces: namespaces, orderStatistics: []) |> mapToSignal { view -> Signal<(Message, [Message])?, NoError> in let position: NavigatedMessageFromViewPosition switch navigation { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 4f00441a9ec..b0cd8739876 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -68,6 +68,8 @@ import BirthdayPickerScreen import StarsTransactionsScreen import StarsPurchaseScreen import StarsTransferScreen +import StarsTransactionScreen +import StarsWithdrawalScreen import NGCore import NGData @@ -550,7 +552,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { sortIndex = sortOrder.order } else if case let .backupData(backupDataValue) = attribute { backupData = backupDataValue.data - } else if case .supportUserInfo = attribute { + } else if case .supportUserInfo = attribute, !"".isEmpty { isSupportUser = true } } @@ -1742,8 +1744,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return peersNearbyController(context: context) } - public func makeChatController(context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, botStart: ChatControllerInitialBotStart?, mode: ChatControllerPresentationMode) -> ChatController { - return ChatControllerImpl(context: context, chatLocation: chatLocation, subject: subject, botStart: botStart, mode: mode) + public func makeChatController(context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, botStart: ChatControllerInitialBotStart?, mode: ChatControllerPresentationMode, params: ChatControllerParams?) -> ChatController { + return ChatControllerImpl(context: context, chatLocation: chatLocation, subject: subject, botStart: botStart, mode: mode, params: params) } public func makeChatHistoryListNode( @@ -1851,7 +1853,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return nil }, chatControllerNode: { return nil - }, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in + }, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return .none }, canSendMessages: { @@ -1906,7 +1908,6 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in }, openStickerEditor: { - }, openPhoneContextMenu: { _ in }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in @@ -2039,8 +2040,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return HashtagSearchController(context: context, peer: peer, query: query, all: all) } - public func makeStorySearchController(context: AccountContext, query: String) -> ViewController { - return StorySearchGridScreen(context: context, searchQuery: query) + public func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController { + return StorySearchGridScreen(context: context, scope: scope, listContext: listContext) } public func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController { @@ -2186,6 +2187,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSource = .storiesSuggestedReactions case .storiesHigherQuality: mappedSource = .storiesHigherQuality + case .storiesLinks: + mappedSource = .storiesLinks case let .channelBoost(peerId): mappedSource = .channelBoost(peerId) case .nameColor: @@ -2202,6 +2205,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSource = .messageTags case .folderTags: mappedSource = .folderTags + case .messageEffects: + mappedSource = .messageEffects case .animatedEmoji: mappedSource = .animatedEmoji } @@ -2258,6 +2263,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSubject = .messagePrivacy case .folderTags: mappedSubject = .folderTags + case .messageEffects: + mappedSubject = .messageEffects case .business: mappedSubject = .business buttonText = presentationData.strings.Chat_EmptyStateIntroFooterPremiumActionButton @@ -2758,17 +2765,29 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: peerId, requiredStars: requiredStars, modal: true, completion: completion) } - public func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController { - return StarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: source, inputData: inputData, completion: completion) + public func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController { + return StarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: source, extendedMedia: extendedMedia, inputData: inputData, completion: completion) } - public func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction) -> ViewController { - return StarsTransactionScreen(context: context, subject: .transaction(transaction), action: {}) + public func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController { + return StarsTransactionScreen(context: context, subject: .transaction(transaction, peer), action: {}) } public func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController { return StarsTransactionScreen(context: context, subject: .receipt(receipt), action: {}) } + + public func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController { + return StarsStatisticsScreen(context: context, peerId: peerId, revenueContext: revenueContext) + } + + public func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController { + return StarsWithdrawScreen(context: context, mode: .paidMedia(initialValue), completion: completion) + } + + public func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController { + return StarsWithdrawScreen(context: context, mode: .withdraw(stats), completion: completion) + } } private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? { diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 38bd5ea5728..0c6a63c0185 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -107,6 +107,15 @@ public final class TelegramRootController: NavigationController, TelegramRootCon private var applicationInFocusDisposable: Disposable? private var storyUploadEventsDisposable: Disposable? + + override public var minimizedContainer: MinimizedContainer? { + didSet { + self.minimizedContainer?.navigationController = self + self.minimizedContainerUpdated(self.minimizedContainer) + } + } + + public var minimizedContainerUpdated: (MinimizedContainer?) -> Void = { _ in } public init(context: AccountContext) { self.context = context @@ -855,6 +864,11 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } } +//Xcode 16 +#if canImport(ContactProvider) +extension MediaEditorScreen.Result: @retroactive MediaEditorScreenResult { +} +#else extension MediaEditorScreen.Result: MediaEditorScreenResult { - } +#endif diff --git a/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift b/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift index 5d5e7e15d46..51b7b85aac8 100644 --- a/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift +++ b/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift @@ -8,6 +8,24 @@ import PhotoResources import ImageCompression public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, media: AnyMediaReference, opportunistic: Bool) -> Signal { + if let paidContent = media.media as? TelegramMediaPaidContent { + var signals: [Signal] = [] + for case let .full(fullMedia) in paidContent.extendedMedia { + signals.append(transformOutgoingMessageMedia(postbox: postbox, network: network, media: media.withUpdatedMedia(fullMedia), opportunistic: opportunistic)) + } + return combineLatest(signals) + |> mapToSignal { results -> Signal in + let mediaResults = results.compactMap { $0?.media } + if mediaResults.count == signals.count { + return .single(media.withUpdatedMedia(TelegramMediaPaidContent(amount: paidContent.amount, extendedMedia: mediaResults.map { .full(media: $0) }))) + } else if opportunistic { + return .single(nil) + } else { + return .complete() + } + } + } + switch media.media { case let file as TelegramMediaFile: let signal = Signal { subscriber in diff --git a/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift index 630b6b118a6..75b0164c34a 100644 --- a/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift @@ -12,8 +12,6 @@ import AccessoryPanelNode import AppBundle final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { - private let webpageDisposable = MetaDisposable() - private(set) var webpage: TelegramMediaWebpage private(set) var url: String @@ -69,11 +67,7 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { self.updateWebpage() } - - deinit { - self.webpageDisposable.dispose() - } - + override func animateIn() { self.iconView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) } diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 540098b578b..ac5d20494ff 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -57,6 +57,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var callV2: Bool public var allowWebViewInspection: Bool public var disableReloginTokens: Bool + public var liveStreamV2: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -92,7 +93,8 @@ public struct ExperimentalUISettings: Codable, Equatable { dustEffect: false, callV2: false, allowWebViewInspection: false, - disableReloginTokens: false + disableReloginTokens: false, + liveStreamV2: false ) } @@ -128,7 +130,8 @@ public struct ExperimentalUISettings: Codable, Equatable { dustEffect: Bool, callV2: Bool, allowWebViewInspection: Bool, - disableReloginTokens: Bool + disableReloginTokens: Bool, + liveStreamV2: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -162,6 +165,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.callV2 = callV2 self.allowWebViewInspection = allowWebViewInspection self.disableReloginTokens = disableReloginTokens + self.liveStreamV2 = liveStreamV2 } public init(from decoder: Decoder) throws { @@ -199,6 +203,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.callV2 = try container.decodeIfPresent(Bool.self, forKey: "callV2") ?? false self.allowWebViewInspection = try container.decodeIfPresent(Bool.self, forKey: "allowWebViewInspection") ?? false self.disableReloginTokens = try container.decodeIfPresent(Bool.self, forKey: "disableReloginTokens") ?? false + self.liveStreamV2 = try container.decodeIfPresent(Bool.self, forKey: "liveStreamV2") ?? false } public func encode(to encoder: Encoder) throws { @@ -236,6 +241,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode(self.callV2, forKey: "callV2") try container.encode(self.allowWebViewInspection, forKey: "allowWebViewInspection") try container.encode(self.disableReloginTokens, forKey: "disableReloginTokens") + try container.encode(self.liveStreamV2, forKey: "liveStreamV2") } } diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index daefa3fa480..ac4f0b7f7b7 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -57,7 +57,7 @@ final class NetworkBroadcastPartSource: BroadcastPartSource { private var dataSource: AudioBroadcastDataSource? #if DEBUG - private let debugDumpDirectory: EngineTempBox.Directory? + private var debugDumpDirectory: EngineTempBox.Directory? #endif init(queue: Queue, engine: TelegramEngine, callId: Int64, accessHash: Int64, isExternalStream: Bool) { @@ -67,8 +67,8 @@ final class NetworkBroadcastPartSource: BroadcastPartSource { self.accessHash = accessHash self.isExternalStream = isExternalStream - #if DEBUG && true - self.debugDumpDirectory = EngineTempBox.shared.tempDirectory() + #if DEBUG + //self.debugDumpDirectory = EngineTempBox.shared.tempDirectory() #endif } @@ -463,7 +463,7 @@ public final class OngoingGroupCallContext { private let audioSessionActiveDisposable = MetaDisposable() - init(queue: Queue, inputDeviceId: String, outputDeviceId: String, audioSessionActive: Signal, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool, logPath: String) { + init(queue: Queue, inputDeviceId: String, outputDeviceId: String, audioSessionActive: Signal, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool, logPath: String, onMutedSpeechActivityDetected: @escaping (Bool) -> Void) { self.queue = queue #if os(iOS) @@ -576,6 +576,9 @@ public final class OngoingGroupCallContext { disableAudioInput: disableAudioInput, preferX264: preferX264, logPath: logPath, + onMutedSpeechActivityDetected: { value in + onMutedSpeechActivityDetected(value) + }, audioDevice: audioDevice ) #else @@ -1109,10 +1112,10 @@ public final class OngoingGroupCallContext { } } - public init(inputDeviceId: String = "", outputDeviceId: String = "", audioSessionActive: Signal, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool, logPath: String) { + public init(inputDeviceId: String = "", outputDeviceId: String = "", audioSessionActive: Signal, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool, logPath: String, onMutedSpeechActivityDetected: @escaping (Bool) -> Void) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { - return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, audioSessionActive: audioSessionActive, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, preferX264: preferX264, logPath: logPath) + return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, audioSessionActive: audioSessionActive, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, preferX264: preferX264, logPath: logPath, onMutedSpeechActivityDetected: onMutedSpeechActivityDetected) }) } diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 52dc6e72924..67f47dff20a 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -407,7 +407,7 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { public enum Custom: Codable { case topic(id: Int64, info: EngineMessageHistoryThread.Info) case nameColors([UInt32]) - case stars + case stars(tinted: Bool) } public let interactivelySelectedFromPackId: ItemCollectionId? diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index d7b772dfd04..8b2c52d3cd9 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -414,6 +414,7 @@ typedef NS_ENUM(int32_t, OngoingGroupCallRequestedVideoQuality) { disableAudioInput:(bool)disableAudioInput preferX264:(bool)preferX264 logPath:(NSString * _Nonnull)logPath +onMutedSpeechActivityDetected:(void (^ _Nullable)(bool))onMutedSpeechActivityDetected audioDevice:(SharedCallAudioDevice * _Nullable)audioDevice; - (void)stop; diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 74f1f6f5a9c..fe3df0061a6 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -1669,6 +1669,8 @@ @interface GroupCallThreadLocalContext () { rtc::Thread *_currentAudioDeviceModuleThread; SharedCallAudioDevice * _audioDevice; + + void (^_onMutedSpeechActivityDetected)(bool); } @end @@ -1691,6 +1693,7 @@ - (instancetype _Nonnull)initWithQueue:(id> audioDeviceModule; if (_audioDevice) { @@ -1917,12 +1922,19 @@ - (instancetype _Nonnull)initWithQueue:(id(task); }, .minOutgoingVideoBitrateKbit = minOutgoingVideoBitrateKbit, - .createAudioDeviceModule = [weakSelf, queue, disableAudioInput, audioDeviceModule](webrtc::TaskQueueFactory *taskQueueFactory) -> rtc::scoped_refptr { + .createAudioDeviceModule = [weakSelf, queue, disableAudioInput, audioDeviceModule, onMutedSpeechActivityDetected = _onMutedSpeechActivityDetected](webrtc::TaskQueueFactory *taskQueueFactory) -> rtc::scoped_refptr { if (audioDeviceModule) { return audioDeviceModule->getSyncAssumingSameThread()->audioDeviceModule(); } else { rtc::Thread *audioDeviceModuleThread = rtc::Thread::Current(); auto resultModule = rtc::make_ref_counted(false, disableAudioInput, disableAudioInput ? 2 : 1); + if (resultModule) { + resultModule->mutedSpeechDetectionChanged = ^(bool value) { + if (onMutedSpeechActivityDetected) { + onMutedSpeechActivityDetected(value); + } + }; + } [queue dispatch:^{ __strong GroupCallThreadLocalContext *strongSelf = weakSelf; if (strongSelf) { @@ -1932,6 +1944,14 @@ - (instancetype _Nonnull)initWithQueue:(id_onMutedSpeechActivityDetected) { + strongSelf->_onMutedSpeechActivityDetected(value); + } + }]; } })); } diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 8721352f452..8344f8ca2a0 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 8721352f452128adec41c254b8407a4cb18cbbeb +Subproject commit 8344f8ca2a043d0812743260c31a086a82190489 diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index 330aa3ea3b8..3abe33c29ac 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -792,7 +792,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { var avatarFrame = animationFrame if let icon, case let .peer(_, isStory) = icon, isStory { - let indicatorTransition: Transition = .immediate + let indicatorTransition: ComponentTransition = .immediate let avatarStoryIndicator: ComponentView if let current = self.avatarStoryIndicator { avatarStoryIndicator = current diff --git a/submodules/TranslateUI/Sources/PlayPauseIconComponent.swift b/submodules/TranslateUI/Sources/PlayPauseIconComponent.swift index ba94aa2efcb..561a0feda14 100644 --- a/submodules/TranslateUI/Sources/PlayPauseIconComponent.swift +++ b/submodules/TranslateUI/Sources/PlayPauseIconComponent.swift @@ -93,7 +93,7 @@ final class PlayPauseIconComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: PlayPauseIconComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func update(component: PlayPauseIconComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { if self.component?.state != component.state { self.component = component @@ -114,7 +114,7 @@ final class PlayPauseIconComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/TranslateUI/Sources/TranslateButtonComponent.swift b/submodules/TranslateUI/Sources/TranslateButtonComponent.swift index 7361ac70d63..bf431c92ac0 100644 --- a/submodules/TranslateUI/Sources/TranslateButtonComponent.swift +++ b/submodules/TranslateUI/Sources/TranslateButtonComponent.swift @@ -151,7 +151,7 @@ final class TranslateButtonComponent: Component { } } - public func update(component: TranslateButtonComponent, availableSize: CGSize, transition: Transition) -> CGSize { + public func update(component: TranslateButtonComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component self.backgroundView.backgroundColor = component.theme.list.itemBlocksBackgroundColor @@ -177,7 +177,7 @@ final class TranslateButtonComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/TranslateUI/Sources/TranslateScreen.swift b/submodules/TranslateUI/Sources/TranslateScreen.swift index 1cc8c69c9d9..d8bafa44cdf 100644 --- a/submodules/TranslateUI/Sources/TranslateScreen.swift +++ b/submodules/TranslateUI/Sources/TranslateScreen.swift @@ -641,7 +641,7 @@ public class TranslateScreen: ViewController { } } - func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) { + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ComponentTransition) { self.currentLayout = (layout, navigationHeight) if let controller = self.controller, let navigationBar = controller.navigationBar, navigationBar.view.superview !== self.wrappingView { @@ -928,11 +928,11 @@ public class TranslateScreen: ViewController { let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) } else { self.isExpanded = true - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) } } else if (velocity.y < -300.0 || offset < topInset / 2.0) { if velocity.y > -2200.0 && velocity.y < -300.0, let listNode = listNode { @@ -945,7 +945,7 @@ public class TranslateScreen: ViewController { let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) self.isExpanded = true - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) } else { if let listNode = listNode { listNode.scroller.setContentOffset(CGPoint(), animated: false) @@ -953,7 +953,7 @@ public class TranslateScreen: ViewController { scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) } - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) } if !dismissing { @@ -966,7 +966,7 @@ public class TranslateScreen: ViewController { case .cancelled: self.panGestureArguments = nil - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) default: break } @@ -981,7 +981,7 @@ public class TranslateScreen: ViewController { guard let (layout, navigationHeight) = self.currentLayout else { return } - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) } } @@ -1163,6 +1163,6 @@ public class TranslateScreen: ViewController { let navigationHeight: CGFloat = 56.0 - self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) } } diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 01fabaa0acb..030e3c3a580 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -39,6 +39,7 @@ public enum UndoOverlayContent { case copy(text: String) case mediaSaved(text: String) case paymentSent(currencyValue: String, itemTitle: String) + case starsSent(context: AccountContext, file: TelegramMediaFile, amount: Int64, title: String, text: String?) case inviteRequestSent(title: String, text: String) case image(image: UIImage, title: String?, text: String, round: Bool, undoText: String?) case notificationSoundAdded(title: String, text: String, action: (() -> Void)?) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 78386b6264d..0d68fcff210 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -380,6 +380,69 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.attributedText = string displayUndo = false self.originalRemainingSeconds = 5 + case let .starsSent(context, file, _, title, text): + self.avatarNode = nil + self.iconNode = nil + self.iconCheckNode = nil + self.animationNode = nil + + let imageBoundingSize = CGSize(width: 34.0, height: 34.0) + + let emojiStatus = ComponentView() + self.emojiStatus = emojiStatus + let _ = emojiStatus.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + content: .animation( + content: .file(file: file), + size: imageBoundingSize, + placeholderColor: UIColor(white: 1.0, alpha: 0.1), + themeColor: .white, + loopMode: .count(1) + ), + isVisibleForAnimations: true, + useSharedAnimation: false, + action: nil + )), + environment: {}, + containerSize: imageBoundingSize + ) + + self.stickerImageSize = imageBoundingSize + + if let text { + let formattedString = text + + let string = NSMutableAttributedString(attributedString: NSAttributedString(string: formattedString, font: Font.regular(14.0), textColor: .white)) + let starRange = (string.string as NSString).range(of: "{star}") + if starRange.location != NSNotFound { + string.replaceCharacters(in: starRange, with: "") + string.insert(NSAttributedString(string: ".", attributes: [ + .font: Font.regular(14.0), + ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: MessageReaction.starsReactionId, file: file, custom: nil) + ]), at: starRange.location) + } + + self.textNode.attributedText = string + self.textNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: UIColor(white: 1.0, alpha: 0.1), + attemptSynchronous: false + ) + self.textNode.visibility = true + } + + //TODO:localize + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + + displayUndo = false + self.originalRemainingSeconds = 3 + isUserInteractionEnabled = true case let .messagesUnpinned(title, text, undo, isHidden): self.avatarNode = nil self.iconNode = nil @@ -1108,7 +1171,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) - let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) + let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor) let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in return ("URL", contents) }), textAlignment: .natural) @@ -1232,7 +1295,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { switch content { case .removedChat: self.panelWrapperNode.addSubnode(self.timerTextNode) - case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal, .premiumPaywall, .peers, .messageTagged: + case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .starsSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal, .premiumPaywall, .peers, .messageTagged: if self.textNode.tapAttributeAction != nil || displayUndo { self.isUserInteractionEnabled = true } else { @@ -1417,32 +1480,41 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { func updateContent(_ content: UndoOverlayContent) { self.content = content + var undoTextColor = self.presentationData.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0) + switch content { - case let .image(image, title, text, _, _): - self.iconNode?.image = image - if let title = title { - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) - } else { - self.titleNode.attributedText = nil - } - self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white) - self.renewWithCurrentContent() - case let .actionSucceeded(title, text, _, destructive): - var undoTextColor = self.presentationData.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0) - if destructive { - undoTextColor = UIColor(rgb: 0xff7b74) - } - - let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) - let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) - let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural) - if let title { - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) - } - self.textNode.attributedText = attributedText - default: - break + case let .info(title, text, _, _), let .universal(_, _, _, title, text, _, _): + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural) + if let title { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + } + self.textNode.attributedText = attributedText + case let .image(image, title, text, _, _): + self.iconNode?.image = image + if let title = title { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + } else { + self.titleNode.attributedText = nil + } + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white) + self.renewWithCurrentContent() + case let .actionSucceeded(title, text, _, destructive): + if destructive { + undoTextColor = UIColor(rgb: 0xff7b74) + } + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural) + if let title { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + } + self.textNode.attributedText = attributedText + default: + break } if let validLayout = self.validLayout { diff --git a/submodules/UrlEscaping/Sources/UrlEscaping.swift b/submodules/UrlEscaping/Sources/UrlEscaping.swift index 165b60ed44c..83a6fed01c9 100644 --- a/submodules/UrlEscaping/Sources/UrlEscaping.swift +++ b/submodules/UrlEscaping/Sources/UrlEscaping.swift @@ -26,7 +26,7 @@ public func isValidUrl(_ url: String, validSchemes: [String: Bool] = ["http": tr if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: escapedUrl), let scheme = url.scheme?.lowercased(), let requiresTopLevelDomain = validSchemes[scheme], let host = url.host, (!requiresTopLevelDomain || host.contains(".")) && url.user == nil { if requiresTopLevelDomain { let components = host.components(separatedBy: ".") - let domain = (components.first ?? "") + let domain = (components.last ?? "") if domain.isEmpty { return false } diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 5b9c30e7d5e..eff23dab0f7 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -71,7 +71,7 @@ public enum ParsedInternalPeerUrlParameter { case channelMessage(Int32, Double?) case replyThread(Int32, Int32) case voiceChat(String?) - case appStart(String, String?) + case appStart(String, String?, Bool) case story(Int32) case boost case text(String) @@ -588,16 +588,19 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, query: String) } else if pathComponents.count == 2 { let appName = pathComponents[1] var startApp: String? + var compact = false if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { if queryItem.name == "startapp" { startApp = value + } else if queryItem.name == "mode", value == "compact" { + compact = true } } } } - return .peer(.name(peerName), .appStart(appName, startApp)) + return .peer(.name(peerName), .appStart(appName, startApp, compact)) } else { return nil } @@ -713,7 +716,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } } } - case let .appStart(name, payload): + case let .appStart(name, payload, compact): return .single(.progress) |> then(context.engine.messages.getBotApp(botId: peer.id, shortName: name, cached: false) |> map(Optional.init) |> `catch` { _ -> Signal in @@ -721,7 +724,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } |> mapToSignal { botApp -> Signal in if let botApp { - return .single(.result(.peer(peer._asPeer(), .withBotApp(ChatControllerInitialBotAppStart(botApp: botApp, payload: payload, justInstalled: false))))) + return .single(.result(.peer(peer._asPeer(), .withBotApp(ChatControllerInitialBotAppStart(botApp: botApp, payload: payload, justInstalled: false, compact: compact))))) } else { return .single(.result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil)))) } @@ -1138,6 +1141,21 @@ public func parseAdUrl(sharedContext: SharedAccountContext, url: String) -> Pars return nil } +public func parseFullInternalUrl(sharedContext: SharedAccountContext, url: String) -> ParsedInternalUrl? { + let schemes = ["http://", "https://", ""] + for basePath in baseTelegramMePaths { + for scheme in schemes { + let basePrefix = scheme + basePath + "/" + if url.lowercased().hasPrefix(basePrefix) { + if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: String(url[basePrefix.endIndex...])) { + return internalUrl + } + } + } + } + return nil +} + private struct UrlHandlingConfiguration { static var defaultValue: UrlHandlingConfiguration { return UrlHandlingConfiguration(domains: [], urlAuthDomains: []) diff --git a/submodules/Utils/VolumeButtons/Sources/VolumeButtons.swift b/submodules/Utils/VolumeButtons/Sources/VolumeButtons.swift index 703dee1d13f..5ac2ff4f617 100644 --- a/submodules/Utils/VolumeButtons/Sources/VolumeButtons.swift +++ b/submodules/Utils/VolumeButtons/Sources/VolumeButtons.swift @@ -16,15 +16,19 @@ private final class LegacyHandlerImpl: VolumeButtonHandlerImpl { context: SharedAccountContext, performAction: @escaping (VolumeButtonsListener.Action) -> Void ) { - self.handler = PGCameraVolumeButtonHandler(upButtonPressedBlock: { - performAction(.up) - }, upButtonReleasedBlock: { - performAction(.upRelease) - }, downButtonPressedBlock: { - performAction(.down) - }, downButtonReleasedBlock: { - performAction(.downRelease) - }) + self.handler = PGCameraVolumeButtonHandler( + isCameraSpecific: false, + eventView: context.mainWindow?.viewController?.view, + upButtonPressedBlock: { + performAction(.up) + }, upButtonReleasedBlock: { + performAction(.upRelease) + }, downButtonPressedBlock: { + performAction(.down) + }, downButtonReleasedBlock: { + performAction(.downRelease) + } + ) self.handler.enabled = true } diff --git a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift index e4a7ce0c921..5c986acfb2c 100644 --- a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift +++ b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift @@ -202,7 +202,7 @@ final class WatchSendMessageHandler: WatchRequestHandler { messageSignal = .single((.message(text: args.text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []), peerId)) } else if let args = subscription as? TGBridgeSendLocationMessageSubscription, let location = args.location { let peerId = makePeerIdFromBridgeIdentifier(args.peerId) - let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: makeVenue(from: location.venue), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, venue: makeVenue(from: location.venue), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) messageSignal = .single((.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: map), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []), peerId)) } else if let args = subscription as? TGBridgeSendStickerMessageSubscription { let peerId = makePeerIdFromBridgeIdentifier(args.peerId) diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 98769d41dab..574919ba3f9 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -208,6 +208,7 @@ public struct WebAppParameters { let buttonText: String? let keepAliveSignal: Signal? let forceHasSettings: Bool + let fullSize: Bool public init( source: Source, @@ -219,7 +220,8 @@ public struct WebAppParameters { payload: String?, buttonText: String?, keepAliveSignal: Signal?, - forceHasSettings: Bool + forceHasSettings: Bool, + fullSize: Bool ) { self.source = source self.peerId = peerId @@ -231,6 +233,7 @@ public struct WebAppParameters { self.buttonText = buttonText self.keepAliveSignal = keepAliveSignal self.forceHasSettings = forceHasSettings + self.fullSize = fullSize } } @@ -288,6 +291,7 @@ public final class WebAppController: ViewController, AttachmentContainable { private let context: AccountContext var presentationData: PresentationData private var queryId: Int64? + fileprivate let canMinimize = true private var placeholderDisposable: Disposable? private var iconDisposable: Disposable? @@ -306,7 +310,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.context = context self.controller = controller self.presentationData = controller.presentationData - + self.backgroundNode = ASDisplayNode() self.headerBackgroundNode = ASDisplayNode() self.topOverscrollNode = ASDisplayNode() @@ -477,33 +481,55 @@ public final class WebAppController: ViewController, AttachmentContainable { guard let strongSelf = self else { return } - if let parsedUrl = URL(string: result) { + if let parsedUrl = URL(string: result.url) { + strongSelf.queryId = result.queryId strongSelf.webView?.load(URLRequest(url: parsedUrl)) } }) } else { - let _ = (self.context.engine.messages.requestWebView(peerId: controller.peerId, botId: controller.botId, url: controller.url, payload: controller.payload, themeParams: generateWebAppThemeParams(presentationData.theme), fromMenu: controller.source == .menu, replyToMessageId: controller.replyToMessageId, threadId: controller.threadId) - |> deliverOnMainQueue).start(next: { [weak self] result in - guard let strongSelf = self else { - return - } - if let parsedUrl = URL(string: result.url) { + if let url = controller.url, isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: self.context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl { + let _ = (self.context.sharedContext.resolveUrl(context: self.context, peerId: controller.peerId, url: url, skipUrlAuth: false) + |> deliverOnMainQueue).startStandalone(next: { [weak self] result in + guard let self, let controller = self.controller else { + return + } + guard case let .peer(peer, params) = result, let peer, case let .withBotApp(appStart) = params else { + controller.dismiss() + return + } + let _ = (self.context.engine.messages.requestAppWebView(peerId: peer.id, appReference: .id(id: appStart.botApp.id, accessHash: appStart.botApp.accessHash), payload: appStart.payload, themeParams: generateWebAppThemeParams(self.presentationData.theme), compact: appStart.compact, allowWrite: true) + |> deliverOnMainQueue).startStandalone(next: { [weak self] result in + guard let self, let parsedUrl = URL(string: result.url) else { + return + } + self.controller?.titleView?.title = CounterControllerTitle(title: appStart.botApp.title, counter: self.presentationData.strings.Bot_GenericBotStatus) + self.webView?.load(URLRequest(url: parsedUrl)) + }) + }) + } else { + let _ = (self.context.engine.messages.requestWebView(peerId: controller.peerId, botId: controller.botId, url: controller.url, payload: controller.payload, themeParams: generateWebAppThemeParams(presentationData.theme), fromMenu: controller.source == .menu, replyToMessageId: controller.replyToMessageId, threadId: controller.threadId) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self, let parsedUrl = URL(string: result.url) else { + return + } strongSelf.queryId = result.queryId strongSelf.webView?.load(URLRequest(url: parsedUrl)) - strongSelf.keepAliveDisposable = (result.keepAliveSignal - |> deliverOnMainQueue).start(error: { [weak self] _ in - if let strongSelf = self { - strongSelf.controller?.dismiss() - } - }, completed: { [weak self] in - if let strongSelf = self { - strongSelf.controller?.completion() - strongSelf.controller?.dismiss() - } - }) - } - }) + if let keepAliveSignal = result.keepAliveSignal { + strongSelf.keepAliveDisposable = (keepAliveSignal + |> deliverOnMainQueue).start(error: { [weak self] _ in + if let strongSelf = self { + strongSelf.controller?.dismiss() + } + }, completed: { [weak self] in + if let strongSelf = self { + strongSelf.controller?.completion() + strongSelf.controller?.dismiss() + } + }) + } + }) + } } } } @@ -882,6 +908,7 @@ public final class WebAppController: ViewController, AttachmentContainable { starsContext: starsContext, invoice: invoice, source: .slug(slug), + extendedMedia: [], inputData: starsInputData, completion: { [weak self] paid in guard let self else { @@ -908,6 +935,11 @@ public final class WebAppController: ViewController, AttachmentContainable { } case "web_app_open_link": if let json = json, let url = json["url"] as? String { + let webAppConfiguration = WebAppConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) + if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: escapedUrl), let scheme = url.scheme?.lowercased(), !["http", "https"].contains(scheme) && !webAppConfiguration.allowedProtocols.contains(scheme) { + return + } + let tryInstantView = json["try_instant_view"] as? Bool ?? false if let lastTouchTimestamp = self.webView?.lastTouchTimestamp, currentTimestamp < lastTouchTimestamp + 10.0 { self.webView?.lastTouchTimestamp = nil @@ -1770,9 +1802,9 @@ public final class WebAppController: ViewController, AttachmentContainable { fileprivate let moreButtonNode: MoreButtonNode private let context: AccountContext - private let source: WebAppParameters.Source + public let source: WebAppParameters.Source private let peerId: PeerId - private let botId: PeerId + public let botId: PeerId private let botName: String private let url: String? private let queryId: Int64? @@ -1876,6 +1908,25 @@ public final class WebAppController: ViewController, AttachmentContainable { self.presentationDataDisposable?.dispose() } + public func beforeMaximize(navigationController: NavigationController, completion: @escaping () -> Void) { + switch self.source { + case .generic, .settings: + completion() + case .inline, .attachMenu, .menu, .simple: + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId) + ) + |> deliverOnMainQueue).start(next: { [weak self] chatPeer in + guard let self, let chatPeer else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(chatPeer), completion: { _ in + completion() + })) + }) + } + } + fileprivate func updateNavigationBarTheme(transition: ContainedViewLayoutTransition) { let navigationBarPresentationData: NavigationBarPresentationData if let backgroundColor = self.controllerNode.headerColor, let textColor = self.controllerNode.headerPrimaryTextColor { @@ -2083,13 +2134,21 @@ public final class WebAppController: ViewController, AttachmentContainable { } } - public func shouldDismissImmediately() -> Bool { - if self.controllerNode.needDismissConfirmation { - return false - } else { - return true + public override var isMinimized: Bool { + didSet { + if self.isMinimized != oldValue && self.isMinimized { + self.controllerNode.webView?.hideScrollIndicators() + } } } + + public func shouldDismissImmediately() -> Bool { + return true + } + + fileprivate var canMinimize: Bool { + return self.controllerNode.canMinimize + } } final class WebAppPickerContext: AttachmentMediaPickerContext { @@ -2167,11 +2226,11 @@ public func standaloneWebAppController( willDismiss: @escaping () -> Void = {}, didDismiss: @escaping () -> Void = {}, getNavigationController: @escaping () -> NavigationController? = { return nil }, - getSourceRect: (() -> CGRect?)? = nil) -> ViewController { - let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: params.peerId), buttons: [.standalone], initialButton: .standalone, fromMenu: params.source == .menu, hasTextInput: false, makeEntityInputView: { + getSourceRect: (() -> CGRect?)? = nil +) -> ViewController { + let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: params.peerId), buttons: [.standalone], initialButton: .standalone, fromMenu: params.source == .menu, hasTextInput: false, isFullSize: params.fullSize, makeEntityInputView: { return nil }) - controller.getInputContainerNode = getInputContainerNode controller.requestController = { _, present in let webAppController = WebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil, threadId: threadId) webAppController.openUrl = openUrl @@ -2183,5 +2242,36 @@ public func standaloneWebAppController( controller.willDismiss = willDismiss controller.didDismiss = didDismiss controller.getSourceRect = getSourceRect + controller.title = params.botName + controller.shouldMinimizeOnSwipe = { [weak controller] _ in + if let controller, let mainController = controller.mainController as? WebAppController { + return mainController.canMinimize + } + return false + } return controller } + +private struct WebAppConfiguration { + static var defaultValue: WebAppConfiguration { + return WebAppConfiguration(allowedProtocols: []) + } + + let allowedProtocols: [String] + + fileprivate init(allowedProtocols: [String]) { + self.allowedProtocols = allowedProtocols + } + + static func with(appConfiguration: AppConfiguration) -> WebAppConfiguration { + if let data = appConfiguration.data { + var allowedProtocols: [String] = [] + if let value = data["web_app_allowed_protocols"] as? [String] { + allowedProtocols = value + } + return WebAppConfiguration(allowedProtocols: allowedProtocols) + } else { + return .defaultValue + } + } +} diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index 169383d6f7d..48c4d5dc8f2 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -194,6 +194,22 @@ final class WebAppWebView: WKWebView { } } + func hideScrollIndicators() { + var hiddenViews: [UIView] = [] + for view in self.scrollView.subviews.reversed() { + let minSize = min(view.frame.width, view.frame.height) + if minSize < 4.0 { + view.isHidden = true + hiddenViews.append(view) + } + } + Queue.mainQueue().after(2.0) { + for view in hiddenViews { + view.isHidden = false + } + } + } + func sendEvent(name: String, data: String?) { let script = "window.TelegramGameProxy.receiveEvent(\"\(name)\", \(data ?? "null"))" self.evaluateJavaScript(script, completionHandler: { _, _ in diff --git a/submodules/WebsiteType/BUILD b/submodules/WebsiteType/BUILD index 376115a9b5f..e91f19bedc1 100644 --- a/submodules/WebsiteType/BUILD +++ b/submodules/WebsiteType/BUILD @@ -10,7 +10,8 @@ swift_library( #"-warnings-as-errors", ], deps = [ - "//submodules/TelegramCore:TelegramCore", + "//submodules/Postbox", + "//submodules/TelegramCore", ], visibility = [ "//visibility:public", diff --git a/submodules/WebsiteType/Sources/WebsiteType.swift b/submodules/WebsiteType/Sources/WebsiteType.swift index a8bf0ad6c15..69407581d21 100644 --- a/submodules/WebsiteType/Sources/WebsiteType.swift +++ b/submodules/WebsiteType/Sources/WebsiteType.swift @@ -1,4 +1,5 @@ import Foundation +import Postbox import TelegramCore public enum WebsiteType { @@ -35,3 +36,29 @@ public func instantPageType(of webpage: TelegramMediaWebpageLoadedContent) -> In return .generic } } + +public func defaultWebpageImageSizeIsSmall(webpage: TelegramMediaWebpageLoadedContent) -> Bool { + let type = websiteType(of: webpage.websiteName) + + let mainMedia: Media? + switch type { + case .instagram, .twitter: + mainMedia = webpage.story ?? webpage.image ?? webpage.file + default: + mainMedia = webpage.story ?? webpage.file ?? webpage.image + } + + if let image = mainMedia as? TelegramMediaImage { + if let type = webpage.type, (["photo", "video", "embed", "gif", "document", "telegram_album"] as [String]).contains(type) { + } else if let type = webpage.type, (["article"] as [String]).contains(type) { + return true + } else if let _ = largestImageRepresentation(image.representations)?.dimensions { + if webpage.instantPage == nil { + return true + } + } + } + + return false +} + diff --git a/swift_deps.bzl b/swift_deps.bzl index 1191de33110..cdcbe9b7fa1 100644 --- a/swift_deps.bzl +++ b/swift_deps.bzl @@ -28,7 +28,7 @@ def swift_dependencies(): # branch: release/1.0.0 swift_package( name = "swiftpkg_core_swift", - commit = "20b7275f60ad80634f056905d7f18292294cd510", + commit = "87eb31396b7ef8962686b1c8de561950efc2673f", dependencies_index = "@//:swift_deps_index.json", remote = "https://github.com/denis15yo/core-swift.git", ) @@ -68,7 +68,7 @@ def swift_dependencies(): # version: 2.6.1 swift_package( name = "swiftpkg_floatingpanel", - commit = "22d46c526084724a718b8c39ab77f12452712cc7", + commit = "29185a47bd9f062c060e097641b863ef07f60ba7", dependencies_index = "@//:swift_deps_index.json", remote = "https://github.com/scenee/FloatingPanel", ) @@ -108,7 +108,7 @@ def swift_dependencies(): # branch: develop swift_package( name = "swiftpkg_nicegram_assistant_ios", - commit = "0985fd5dfae1676121c54c31fe2817059d5bf784", + commit = "1b98221095de9a80b44e93ce9bfc699c720d5c00", dependencies_index = "@//:swift_deps_index.json", remote = "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", ) @@ -116,7 +116,7 @@ def swift_dependencies(): # branch: develop swift_package( name = "swiftpkg_nicegram_wallet_ios", - commit = "241a210ee1b5cb9cb95d9245a4ed384973ee2ef2", + commit = "3573f8ef65b8667b5bc927bfa558ffb39f2bd579", dependencies_index = "@//:swift_deps_index.json", remote = "git@bitbucket.org:mobyrix/nicegram-wallet-ios.git", ) @@ -140,7 +140,7 @@ def swift_dependencies(): # version: 5.15.5 swift_package( name = "swiftpkg_sdwebimage", - commit = "b8523c1642f3c142b06dd98443ea7c48343a4dfd", + commit = "be0bcd7823ce56629948491f2eaeaa19979514f7", dependencies_index = "@//:swift_deps_index.json", remote = "https://github.com/SDWebImage/SDWebImage.git", ) @@ -196,7 +196,7 @@ def swift_dependencies(): # version: 1.1.0 swift_package( name = "swiftpkg_swift_collections", - commit = "ee97538f5b81ae89698fd95938896dec5217b148", + commit = "3d2dc41a01f9e49d84f0a3925fb858bed64f702d", dependencies_index = "@//:swift_deps_index.json", remote = "https://github.com/apple/swift-collections", ) @@ -300,7 +300,7 @@ def swift_dependencies(): # version: 4.0.36 swift_package( name = "swiftpkg_wallet_core", - commit = "94116a24445c2052edbc7203baf68296c68ce8f4", + commit = "352e0833678151ae124e1e70d325fc056d76d150", dependencies_index = "@//:swift_deps_index.json", remote = "https://github.com/trustwallet/wallet-core.git", ) diff --git a/swift_deps_index.json b/swift_deps_index.json index c5b0831bccf..0dbe77c7b43 100644 --- a/swift_deps_index.json +++ b/swift_deps_index.json @@ -2511,7 +2511,7 @@ "name": "swiftpkg_core_swift", "identity": "core-swift", "remote": { - "commit": "20b7275f60ad80634f056905d7f18292294cd510", + "commit": "87eb31396b7ef8962686b1c8de561950efc2673f", "remote": "https://github.com/denis15yo/core-swift.git", "branch": "release/1.0.0" } @@ -2556,9 +2556,9 @@ "name": "swiftpkg_floatingpanel", "identity": "floatingpanel", "remote": { - "commit": "22d46c526084724a718b8c39ab77f12452712cc7", + "commit": "29185a47bd9f062c060e097641b863ef07f60ba7", "remote": "https://github.com/scenee/FloatingPanel", - "version": "2.8.3" + "version": "2.8.4" } }, { @@ -2601,7 +2601,7 @@ "name": "swiftpkg_nicegram_assistant_ios", "identity": "nicegram-assistant-ios", "remote": { - "commit": "0985fd5dfae1676121c54c31fe2817059d5bf784", + "commit": "1b98221095de9a80b44e93ce9bfc699c720d5c00", "remote": "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", "branch": "develop" } @@ -2610,7 +2610,7 @@ "name": "swiftpkg_nicegram_wallet_ios", "identity": "nicegram-wallet-ios", "remote": { - "commit": "241a210ee1b5cb9cb95d9245a4ed384973ee2ef2", + "commit": "3573f8ef65b8667b5bc927bfa558ffb39f2bd579", "remote": "git@bitbucket.org:mobyrix/nicegram-wallet-ios.git", "branch": "develop" } @@ -2637,9 +2637,9 @@ "name": "swiftpkg_sdwebimage", "identity": "sdwebimage", "remote": { - "commit": "b8523c1642f3c142b06dd98443ea7c48343a4dfd", + "commit": "be0bcd7823ce56629948491f2eaeaa19979514f7", "remote": "https://github.com/SDWebImage/SDWebImage.git", - "version": "5.19.3" + "version": "5.19.4" } }, { @@ -2700,9 +2700,9 @@ "name": "swiftpkg_swift_collections", "identity": "swift-collections", "remote": { - "commit": "ee97538f5b81ae89698fd95938896dec5217b148", + "commit": "3d2dc41a01f9e49d84f0a3925fb858bed64f702d", "remote": "https://github.com/apple/swift-collections", - "version": "1.1.1" + "version": "1.1.2" } }, { @@ -2817,9 +2817,9 @@ "name": "swiftpkg_wallet_core", "identity": "wallet-core", "remote": { - "commit": "94116a24445c2052edbc7203baf68296c68ce8f4", + "commit": "352e0833678151ae124e1e70d325fc056d76d150", "remote": "https://github.com/trustwallet/wallet-core.git", - "version": "4.0.46" + "version": "4.0.49" } }, { diff --git a/third-party/opus/build-opus-bazel.sh b/third-party/opus/build-opus-bazel.sh index 06d84d5cd8b..cbeb0189105 100755 --- a/third-party/opus/build-opus-bazel.sh +++ b/third-party/opus/build-opus-bazel.sh @@ -46,7 +46,7 @@ SDK_PATH="$(xcrun --sdk $PLATFORM --show-sdk-path 2>/dev/null)" mkdir -p "${INTERDIR}" -./configure --disable-shared --enable-static --with-pic --disable-extra-programs --disable-doc --disable-asm --enable-intrinsics --enable-deep-plc --enable-dred --enable-osce ${EXTRA_CONFIG} \ +./configure --disable-shared --enable-static --with-pic --disable-extra-programs --disable-doc --disable-asm --enable-intrinsics ${EXTRA_CONFIG} \ --prefix="${INTERDIR}" \ LDFLAGS="$LDFLAGS ${OPT_LDFLAGS} -fPIE -miphoneos-version-min=${MINIOSVERSION} -L${OUTPUTDIR}/lib" \ CFLAGS="$CFLAGS ${EXTRA_CFLAGS} ${OPT_CFLAGS} -fPIE -miphoneos-version-min=${MINIOSVERSION} -I${OUTPUTDIR}/include -isysroot ${SDK_PATH}" \ diff --git a/versions.json b/versions.json index 674962c5c36..4a57a1fadf7 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "1.6.4", + "app": "1.6.5", "xcode": "15.2", "bazel": "7.1.1", "macos": "13.0"

WgM4-bT!~x;v{ey-!5VgmUkumg#4qf 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
Xe^yu|M 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