From 3f922f2333de9b1bacf5749acaf904550192e0fb Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 23 Jan 2026 20:03:54 +0400 Subject: [PATCH] Support failure animation. --- Telegram/Resources/animations/bomb.tgs | Bin 0 -> 49236 bytes Telegram/Resources/langs/lang.strings | 1 + .../Resources/qrc/telegram/animations.qrc | 1 + .../boxes/star_gift_craft_animation.cpp | 792 +++++++++++------- .../boxes/star_gift_craft_animation.h | 30 +- .../SourceFiles/boxes/star_gift_craft_box.cpp | 60 +- .../SourceFiles/boxes/star_gift_craft_box.h | 3 +- .../settings/settings_credits_graphics.cpp | 6 +- .../window/window_session_controller.cpp | 4 +- 9 files changed, 558 insertions(+), 339 deletions(-) create mode 100644 Telegram/Resources/animations/bomb.tgs diff --git a/Telegram/Resources/animations/bomb.tgs b/Telegram/Resources/animations/bomb.tgs new file mode 100644 index 0000000000000000000000000000000000000000..62fc72a4e5568d42082452ed8614b41b1b4f7ac1 GIT binary patch literal 49236 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 MABj!+xY8O004Y^O-2eap literal 0 HcmV?d00001 diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 5b82702fa2..bf6ebaeb1f 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4122,6 +4122,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_craft_progress" = "Crafting"; "lng_gift_craft_progress_chance" = "{percent} success chance"; "lng_gift_craft_progress_fails" = "If crafting fails, all selected gifts will be consumed."; +"lng_gift_craft_failed" = "Crafting failed. Gifts consumed :("; "lng_auction_about_title" = "Auction"; "lng_auction_about_subtitle" = "Join the battle for exclusive gifts."; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index 3902bfff8b..6b319f4bb5 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -55,6 +55,7 @@ ../../animations/passkeys.tgs ../../animations/ban.tgs ../../animations/cocoon.tgs + ../../animations/bomb.tgs ../../animations/profile/profile_muting.tgs ../../animations/profile/profile_unmuting.tgs diff --git a/Telegram/SourceFiles/boxes/star_gift_craft_animation.cpp b/Telegram/SourceFiles/boxes/star_gift_craft_animation.cpp index 32d28dbcd4..810f85c36e 100644 --- a/Telegram/SourceFiles/boxes/star_gift_craft_animation.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_craft_animation.cpp @@ -8,7 +8,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/star_gift_craft_animation.h" #include "base/call_delayed.h" +#include "chat_helpers/stickers_lottie.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_session.h" +#include "history/view/media/history_view_sticker_player.h" #include "info/peer_gifts/info_peer_gifts_common.h" +#include "lottie/lottie_common.h" +#include "main/main_session.h" #include "ui/image/image_prepare.h" #include "ui/top_background_gradient.h" #include "ui/painter.h" @@ -27,6 +34,10 @@ constexpr auto kFlightDuration = crl::time(400); constexpr auto kLoadingFadeInDuration = crl::time(150); constexpr auto kLoadingFadeOutDuration = crl::time(300); constexpr auto kLoadingMinDuration = crl::time(600); +constexpr auto kFailurePlayerStopFrame = 120; +constexpr auto kFailurePlayerFps = 60; +constexpr auto kFailurePlayerFadeOutFrames = 10; +constexpr auto kFailureScaleDuration = crl::time(200); struct Rotation { float64 x = 0.; @@ -42,18 +53,6 @@ struct Rotation { using RotationFn = Fn; -[[nodiscard]] Rotation RotateImpulse(Rotation impulse, Rotation orientation) { - const auto cosX = std::cos(orientation.x); - const auto sinX = std::sin(orientation.x); - const auto cosY = std::cos(orientation.y); - const auto sinY = std::sin(orientation.y); - - return { - impulse.x * cosY - impulse.y * sinX * sinY, - impulse.y * cosX, - }; -} - [[nodiscard]] RotationFn DecayingRotation(Rotation impulse, float64 decay) { const auto lambda = -std::log(decay) / kFrameDuration; return [=](Rotation initial, crl::time t) { @@ -62,7 +61,7 @@ using RotationFn = Fn; }; } -[[nodiscard]] RotationFn DecayingRotationFinal( +[[nodiscard]] RotationFn DecayingEndRotation( Rotation impulse, float64 decay, Rotation target, @@ -86,100 +85,53 @@ struct GiftAnimationConfig { RotationFn rotation; crl::time duration = 0; crl::time revealTime = 0; - int nextFaceIndex = 0; - int nextFaceRotation = 0; + int targetFaceIndex = 0; + int targetFaceRotation = 0; }; -const auto kGiftAnimations = std::array{{ - { DecayingRotationFinal({ 4.1, -8.2 }, 0.98, { 1, -2 }, 2200), 2200, 600, 1, 180 }, - { DecayingRotation({ 3.2, -5.9 }, 0.97), 1200, 0, 4, 180 }, - { DecayingRotationFinal({ 6.2, 7.8 }, 0.98, { 2, 1 }, 2200), 2200, 1000, 1, 0 }, - { DecayingRotation({ 7.0, 4.5 }, 0.97), 1200, 200, 5, 0 }, - { DecayingRotationFinal({ -6.5, 5.0 }, 0.98, { 0, 1 }, 2200), 2200, 800, 1, 0 }, - { DecayingRotation({ -2.5, 3.5 }, 0.98), 1200, 200, 2, 180 }, - { DecayingRotationFinal({ -4.4, -8.2 }, 0.98, { 0, -1.5 }, 2200), 2200, 600, 3, 0 }, -}}; - -[[nodiscard]] int GetFrontFaceAtRotation(float64 rotationX, float64 rotationY) { - struct Vec3 { - float64 x, y, z; - }; - - const auto rotateY = [](Vec3 v, float64 angle) { - const auto c = std::cos(angle); - const auto s = std::sin(angle); - return Vec3{ v.x * c + v.z * s, v.y, -v.x * s + v.z * c }; - }; - const auto rotateX = [](Vec3 v, float64 angle) { - const auto c = std::cos(angle); - const auto s = std::sin(angle); - return Vec3{ v.x, v.y * c - v.z * s, v.y * s + v.z * c }; - }; - - constexpr auto normals = std::array{{ - { 0., 0., -1. }, - { 0., 0., 1. }, - { -1., 0., 0. }, - { 1., 0., 0. }, - { 0., -1., 0. }, - { 0., 1., 0. }, - }}; - - auto bestFace = 0; - auto bestZ = 0.; - for (auto i = 0; i != 6; ++i) { - const auto transformed = rotateX(rotateY(normals[i], rotationY), rotationX); - if (i == 0 || transformed.z < bestZ) { - bestFace = i; - bestZ = transformed.z; - } - } - return bestFace; -} - -[[nodiscard]] int ComputeFaceRotation( - float64 rotationX, - float64 rotationY, - int faceIndex) { - struct Vec3 { - float64 x, y, z; - }; - - const auto rotateY = [](Vec3 v, float64 angle) { - const auto c = std::cos(angle); - const auto s = std::sin(angle); - return Vec3{ v.x * c + v.z * s, v.y, -v.x * s + v.z * c }; - }; - const auto rotateX = [](Vec3 v, float64 angle) { - const auto c = std::cos(angle); - const auto s = std::sin(angle); - return Vec3{ v.x, v.y * c - v.z * s, v.y * s + v.z * c }; - }; - - constexpr auto faceUpVectors = std::array{{ - { 0., -1., 0. }, - { 0., -1., 0. }, - { 0., -1., 0. }, - { 0., -1., 0. }, - { 0., 0., -1. }, - { 0., 0., 1. }, - }}; - - const auto faceUp = rotateX( - rotateY(faceUpVectors[faceIndex], rotationY), - rotationX); - - const auto screenUpY = faceUp.y; - const auto screenUpX = faceUp.x; - - auto rotation = 0; - if (std::abs(screenUpY) >= std::abs(screenUpX)) { - rotation = (screenUpY < 0.) ? 0 : 180; - } else { - rotation = (screenUpX < 0.) ? 90 : 270; - } - return rotation; -} +const auto kGiftAnimations = std::array{{ { + .rotation = DecayingEndRotation({ 4.1, -8.2 }, 0.98, { 1, -2 }, 2200), + .duration = 2200, + .revealTime = 600, + .targetFaceIndex = 1, + .targetFaceRotation = 180, +}, { + .rotation = DecayingRotation({ 3.2, -5.9 }, 0.97), + .duration = 1200, + .revealTime = 0, + .targetFaceIndex = 4, + .targetFaceRotation = 180, +}, { + .rotation = DecayingEndRotation({ 6.2, 7.8 }, 0.98, { 2, 1 }, 2200), + .duration = 2200, + .revealTime = 1000, + .targetFaceIndex = 1, + .targetFaceRotation = 0, +}, { + .rotation = DecayingRotation({ 7.0, 4.5 }, 0.97), + .duration = 1200, + .revealTime = 200, + .targetFaceIndex = 5, + .targetFaceRotation = 0, +}, { + .rotation = DecayingEndRotation({ -6.5, 5.0 }, 0.98, { 0, 1 }, 2200), + .duration = 2200, + .revealTime = 800, + .targetFaceIndex = 1, + .targetFaceRotation = 0, +}, { + .rotation = DecayingRotation({ -2.5, 3.5 }, 0.98), + .duration = 1200, + .revealTime = 200, + .targetFaceIndex = 2, + .targetFaceRotation = 180, +}, { + .rotation = DecayingEndRotation({ -4.4, -8.2 }, 0.98, { 0, -1.5 }, 2200), + .duration = 2200, + .revealTime = 600, + .targetFaceIndex = 3, + .targetFaceRotation = 0, +} }}; [[nodiscard]] QImage CreateBgGradient( QSize size, @@ -303,14 +255,18 @@ const auto kGiftAnimations = std::array{{ const auto &face = faces[faceIndex]; - auto transformedNormal = rotateX(rotateY(face.normal, rotationY), rotationX); + const auto transformedNormal = rotateX( + rotateY(face.normal, rotationY), + rotationX); if (transformedNormal.z >= 0.) { return std::nullopt; } auto result = std::array(); for (auto i = 0; i != 4; ++i) { - auto p = rotateX(rotateY(face.corners[i], rotationY), rotationX); + const auto p = rotateX( + rotateY(face.corners[i], rotationY), + rotationX); const auto viewZ = p.z + half + kFocalLength; if (viewZ <= 0.) { @@ -364,7 +320,7 @@ void PaintCubeFace( rotatedCorners[3] }); - QTransform transform; + auto transform = QTransform(); if (!QTransform::quadToQuad(srcRect, dstRect, transform)) { return; } @@ -415,7 +371,9 @@ void PaintCubeFace( visible.reserve(3); for (auto i = 0; i != 6; ++i) { - auto transformed = rotateX(rotateY(normals[i], rotationY), rotationX); + const auto transformed = rotateX( + rotateY(normals[i], rotationY), + rotationX); if (transformed.z < 0.) { visible.push_back({ i, transformed.z }); } @@ -433,10 +391,10 @@ void PaintCubeFace( void PaintCubeFirstFlight( QPainter &p, - const CraftAnimationState &animState, + not_null state, float64 progress, bool skipForgeIcon = false) { - const auto &shared = animState.shared; + const auto shared = state->shared.get(); const auto overlayBg = shared->forgeBgOverlay; auto sideBg = shared->forgeBg1; @@ -457,74 +415,227 @@ void PaintCubeFirstFlight( } } +std::unique_ptr SetupFailureAnimation( + not_null canvas, + not_null state) { + auto result = std::make_unique(); + const auto raw = result.get(); + + const auto document + = raw->document + = ChatHelpers::GenerateLocalTgsSticker( + state->shared->session, + u"bomb"_q); + const auto media = document->createMediaView(); + media->checkStickerLarge(); + + rpl::single() | rpl::then( + document->session().downloaderTaskFinished() + ) | rpl::filter([=] { + return media->loaded(); + }) | rpl::take(1) | rpl::on_next([=] { + const auto sticker = document->sticker(); + Assert(sticker != nullptr); + + const auto size = state->shared->forgeRect.size(); + raw->player = std::make_unique( + ChatHelpers::LottiePlayerFromDocument( + media.get(), + ChatHelpers::StickerLottieSize::InlineResults, + size, + Lottie::Quality::High)); + raw->player->setRepaintCallback([=] { canvas->update(); }); + }, raw->lifetime); + + return result; +} + +[[nodiscard]] float64 FailureAnimationScale( + not_null state) { + const auto animation = state->failureAnimation.get(); + return animation + ? animation->scaleAnimation.value(animation->scaleStarted ? 0. : 1.) + : 1.; +} + +void FailureAnimationCheck( + not_null state, + not_null canvas) { + const auto animation = state->failureAnimation.get(); + if (!animation) { + return; + } + const auto &config = kGiftAnimations[state->currentPhaseIndex]; + const auto elapsed = (crl::now() - state->animationStartTime); + const auto failureAnimationDuration = (1000 * kFailurePlayerStopFrame) + / kFailurePlayerFps; + if (!animation->playerStarted + && elapsed >= config.duration - failureAnimationDuration) { + animation->playerStarted = true; + } + if (!animation->scaleStarted + && elapsed >= config.duration - kFailureScaleDuration) { + animation->scaleStarted = true; + animation->scaleAnimation.start([=] { + canvas->update(); + }, 1., 0., kFailureScaleDuration, anim::easeInCubic); + } +} + +[[nodiscard]] std::optional FailureAnimationPrepareFrame( + not_null state, + not_null canvas, + int faceIndex) { + if (!state->failureAnimation) { + state->failureAnimation = SetupFailureAnimation( + canvas, + state); + } + const auto shared = state->shared.get(); + const auto animation = state->failureAnimation.get(); + if (!animation->player->ready()) { + return {}; + } else if (shared->finalSide.frame.isNull()) { + shared->finalSide = shared->forgeSides[faceIndex]; + shared->finalSide.frame.fill(shared->finalSide.bg); + } + if (animation->frame.isNull()) { + animation->frame = shared->finalSide.frame; + } + animation->frame.fill(Qt::transparent); + auto q = QPainter(&animation->frame); + const auto paused = !animation->playerStarted; + auto info = animation->player->frame( + st::giftBoxStickerTiny * 2, + QColor(0, 0, 0, 0), + false, + crl::now(), + paused); + const auto frame = info.image; + const auto finished + = animation->playerFinished + = (info.index + 1 == kFailurePlayerStopFrame); + if (finished) { + return 0.; + } + const auto result = (kFailurePlayerStopFrame - info.index) + / float64(kFailurePlayerFadeOutFrames); + if (!paused) { + animation->player->markFrameShown(); + } + const auto outer = animation->frame.size() + / animation->frame.devicePixelRatio(); + const auto size = frame.size() / style::DevicePixelRatio(); + q.drawImage( + QRect( + (outer.width() - size.width()) / 2, + (outer.height() - size.width()) / 2, + size.width(), + size.height()), + frame); + q.end(); + return result; +} + void PaintCube( QPainter &p, - CraftAnimationState *animState, + not_null state, + not_null canvas, QPointF center, float64 size) { - const auto &shared = animState->shared; + const auto shared = state->shared.get(); - const auto faces = GetVisibleCubeFaces(animState->rotationX, animState->rotationY); + FailureAnimationCheck(state, canvas); - if (!animState->nextFaceRevealed - && animState->currentConfigIndex >= 0) { - const auto &config = kGiftAnimations[animState->currentConfigIndex]; - const auto elapsed = (crl::now() - animState->animationStartTime); + const auto totalScale = FailureAnimationScale(state); + const auto showCube = (totalScale > 0.); + const auto scaledCube = showCube && (totalScale < 1.); + if (scaledCube) { + p.save(); + p.translate(center); + p.scale(totalScale, totalScale); + p.translate(-center); + } + + const auto faces = GetVisibleCubeFaces( + state->rotationX, + state->rotationY); + + if (!state->nextFaceRevealed + && state->currentPhaseIndex >= 0) { + const auto &config = kGiftAnimations[state->currentPhaseIndex]; + const auto elapsed = (crl::now() - state->animationStartTime); if (elapsed >= config.revealTime * anim::SlowMultiplier()) { - animState->nextFaceRevealed = true; + state->nextFaceRevealed = true; } } + const auto craftFailed = state->craftResult.has_value() + && !state->craftResult->get(); + + auto paintFailure = Fn(); for (const auto faceIndex : faces) { const auto corners = ComputeCubeFaceCorners( center, size, - animState->rotationX, - animState->rotationY, + state->rotationX, + state->rotationY, faceIndex); if (!corners) { continue; } - const auto thisFaceRevealed = animState->nextFaceRevealed - && (faceIndex == animState->nextFaceIndex); + const auto thisFaceRevealed = state->nextFaceRevealed + && (faceIndex == state->nextFaceIndex); auto faceImage = shared->forgeSides[faceIndex].frame; auto faceRotation = thisFaceRevealed - ? animState->nextFaceRotation + ? state->nextFaceRotation : 0; for (auto i = 0; i < 4; ++i) { - if (i != animState->currentlyFlying - && animState->giftToSide[i].face == faceIndex + if (i != state->currentlyFlying + && state->giftToSide[i].face == faceIndex && shared->corners[i].giftButton) { faceImage = shared->corners[i].gift(1.); - faceRotation = animState->giftToSide[i].rotation; + faceRotation = state->giftToSide[i].rotation; break; } } - if (thisFaceRevealed && animState->allGiftsLanded) { - if (shared->finalSide.frame.isNull()) { - shared->finalSide = shared->forgeSides[faceIndex]; - shared->finalSide.frame.fill(shared->finalSide.bg); - - auto p = QPainter(&shared->finalSide.frame); - const auto side = shared->finalSide.frame.width() / 6; - p.setPen(Qt::NoPen); - p.setBrush(Qt::red); - p.drawEllipse(side, side, side, side); + if (thisFaceRevealed && state->allGiftsLanded) { + if (craftFailed) { + const auto maybeOpacity = FailureAnimationPrepareFrame( + state, + canvas, + faceIndex); + if (maybeOpacity) { + if (const auto opacity = *maybeOpacity; opacity > 0.) { + paintFailure = [=, &p] { + if (opacity < 1.) { + p.setOpacity(opacity); + } + PaintCubeFace( + p, + state->failureAnimation->frame, + *corners, + faceRotation); + }; + } + faceImage = shared->finalSide.frame; + } } - faceImage = shared->finalSide.frame; } - - PaintCubeFace(p, faceImage, *corners, faceRotation); + if (showCube) { + PaintCubeFace(p, faceImage, *corners, faceRotation); + } } - const auto elapsed = (crl::now() - animState->animationStartTime) - / anim::SlowMultiplier(); - p.setPen(Qt::white); - p.setFont(st::semiboldFont); - p.drawText(0, st::semiboldFont->ascent, QString::number(elapsed, 'f', 2)); + if (scaledCube) { + p.restore(); + } + if (paintFailure) { + paintFailure(); + } } struct GiftFlightPosition { @@ -540,11 +651,16 @@ struct GiftFlightPosition { float64 startOffsetY) { const auto eased = progress; - const auto startOrigin = QPointF(originalRect.topLeft()) + QPointF(0, startOffsetY); - const auto targetOrigin = targetCenter - QPointF(targetSize, targetSize) / 2.; - const auto currentOrigin = startOrigin + (targetOrigin - startOrigin) * eased; + const auto startOrigin = QPointF(originalRect.topLeft()) + + QPointF(0, startOffsetY); + const auto targetOrigin = targetCenter + - QPointF(targetSize, targetSize) / 2.; + const auto currentOrigin = startOrigin + + (targetOrigin - startOrigin) * eased; - const auto originalSize = std::max(originalRect.width(), originalRect.height()); + const auto originalSize = std::max( + originalRect.width(), + originalRect.height()); const auto endScale = (originalSize > 0.) ? (targetSize / float64(originalSize)) : 1.; @@ -557,7 +673,7 @@ struct GiftFlightPosition { } [[nodiscard]] FacePlacement GetCameraFacingFreeFace( - const CraftAnimationState &state) { + not_null state) { struct Vec3 { float64 x, y, z; }; @@ -592,7 +708,7 @@ struct GiftFlightPosition { }}; const auto isOccupied = [&](int faceIndex) { - for (const auto &placement : state.giftToSide) { + for (const auto &placement : state->giftToSide) { if (placement.face == faceIndex) { return true; } @@ -608,8 +724,8 @@ struct GiftFlightPosition { continue; } const auto transformed = rotateX( - rotateY(normals[i], state.rotationY), - state.rotationX); + rotateY(normals[i], state->rotationY), + state->rotationX); if (bestFace < 0 || transformed.z < bestZ) { bestFace = i; bestZ = transformed.z; @@ -621,8 +737,8 @@ struct GiftFlightPosition { } const auto faceUp = rotateX( - rotateY(faceUpVectors[bestFace], state.rotationY), - state.rotationX); + rotateY(faceUpVectors[bestFace], state->rotationY), + state->rotationX); const auto screenUpY = faceUp.y; const auto screenUpX = faceUp.x; @@ -708,24 +824,34 @@ void PaintSlideOutCorner( const auto giftTopLeft = currentTopLeft; const auto &extend = st::defaultDropdownMenu.wrap.shadow.extend; - const auto innerTopLeft = giftTopLeft + QPointF(extend.left(), extend.top()); - const auto innerWidth = giftSize.width() - extend.left() - extend.right(); + const auto innerTopLeft = giftTopLeft + + QPointF(extend.left(), extend.top()); + const auto innerWidth = giftSize.width() + - extend.left() + - extend.right(); p.drawImage(giftTopLeft, frame); const auto badgeFade = std::clamp(1. - progress / 0.7, 0., 1.); if (!corner.percentBadge.isNull() && badgeFade > 0.) { - const auto badgeSize = QSizeF(corner.percentBadge.size()) / ratio; - const auto badgePos = innerTopLeft - QPointF(badgeSize.width() / 3., badgeSize.height() / 3.); + const auto badgeSize = QSizeF(corner.percentBadge.size()) + / ratio; + const auto badgePos = innerTopLeft + - QPointF(badgeSize.width() / 3., badgeSize.height() / 3.); p.setOpacity(badgeFade); p.drawImage(badgePos, corner.percentBadge); p.setOpacity(1.); } if (!corner.removeButton.isNull() && badgeFade > 0.) { - const auto removeSize = QSizeF(corner.removeButton.size()) / ratio; + const auto removeSize = QSizeF(corner.removeButton.size()) + / ratio; const auto removePos = innerTopLeft - + QPointF(innerWidth - removeSize.width() + removeSize.width() / 3., -removeSize.height() / 3.); + + QPointF( + (innerWidth + - removeSize.width() + + removeSize.width() / 3.), + -removeSize.height() / 3.); p.setOpacity(badgeFade); p.drawImage(removePos, corner.removeButton); p.setOpacity(1.); @@ -741,49 +867,54 @@ void PaintSlideOutCorner( void PaintSlideOutPhase( QPainter &p, - const std::shared_ptr &state, + not_null shared, QSize canvasSize, float64 progress) { const auto ratio = style::DevicePixelRatio(); - if (!state->topPart.isNull()) { - const auto topSize = QSizeF(state->topPart.size()) / ratio; + if (!shared->topPart.isNull()) { + const auto topSize = QSizeF(shared->topPart.size()) / ratio; const auto slideDistance = topSize.height(); const auto offsetY = slideDistance * progress; const auto opacity = 1. - progress; p.setOpacity(opacity); - p.drawImage(state->topPartRect.topLeft() - QPointF(0, offsetY), state->topPart); + p.drawImage( + shared->topPartRect.topLeft() - QPointF(0, offsetY), + shared->topPart); p.setOpacity(1.); } - if (!state->bottomPart.isNull()) { - const auto slideDistance = QSizeF(state->bottomPart.size()).height() / ratio; + if (!shared->bottomPart.isNull()) { + const auto slideDistance = QSizeF(shared->bottomPart.size()).height() + / ratio; const auto offsetY = slideDistance * progress; const auto opacity = 1. - progress; p.setOpacity(opacity); - p.drawImage(QPointF(0, state->bottomPartY + offsetY), state->bottomPart); + p.drawImage( + QPointF(0, shared->bottomPartY + offsetY), + shared->bottomPart); p.setOpacity(1.); } - const auto offset = state->craftingOffsetY; - for (const auto &corner : state->corners) { + const auto offset = shared->craftingOffsetY; + for (const auto &corner : shared->corners) { PaintSlideOutCorner(p, corner, offset, progress); } auto hq = PainterHighQualityEnabler(p); - const auto forge = state->forgeRect.translated(0, offset); + const auto forge = shared->forgeRect.translated(0, offset); const auto radius = st::boxRadius; p.setPen(Qt::NoPen); - p.setBrush(state->forgeBgOverlay); + p.setBrush(shared->forgeBgOverlay); p.drawRoundedRect(forge, radius, radius); st::craftForge.paintInCenter(p, forge, st::white->c); p.setOpacity(1. - progress); p.drawImage( forge.x() + st::craftForgePadding, forge.y() + st::craftForgePadding, - state->forgePercent); + shared->forgePercent); } [[nodiscard]] std::array RotateCornersForFace( @@ -802,10 +933,10 @@ void PaintSlideOutPhase( } void StartGiftFlight( - CraftAnimationState *animState, + not_null state, not_null canvas, int startIndex) { - const auto &corners = animState->shared->corners; + const auto &corners = state->shared->corners; auto nextGift = -1; for (auto i = startIndex; i < 4; ++i) { @@ -819,32 +950,32 @@ void StartGiftFlight( return; } - animState->currentlyFlying = nextGift; - animState->giftToSide[nextGift] = GetCameraFacingFreeFace(*animState); - animState->flightTargetCorners = std::nullopt; + state->currentlyFlying = nextGift; + state->giftToSide[nextGift] = GetCameraFacingFreeFace(state); + state->flightTargetCorners = std::nullopt; - animState->flightAnimation.start( + state->flightAnimation.start( [=] { canvas->update(); }, 0., 1., kFlightDuration, anim::easeInCubic); - if (!animState->continuousAnimation.animating()) { - animState->continuousAnimation.start(); + if (!state->continuousAnimation.animating()) { + state->continuousAnimation.start(); } } void StartGiftFlightToFace( - CraftAnimationState *animState, + not_null state, not_null canvas, int targetFace) { - const auto shared = animState->shared; + const auto shared = state->shared.get(); const auto &corners = shared->corners; auto nextCorner = -1; for (auto i = 0; i < 4; ++i) { - if (corners[i].giftButton && animState->giftToSide[i].face < 0) { + if (corners[i].giftButton && state->giftToSide[i].face < 0) { nextCorner = i; break; } @@ -854,19 +985,19 @@ void StartGiftFlightToFace( return; } - animState->currentlyFlying = nextCorner; + state->currentlyFlying = nextCorner; - const auto faceRotation = animState->nextFaceRotation; - animState->giftToSide[nextCorner] = FacePlacement{ + const auto faceRotation = state->nextFaceRotation; + state->giftToSide[nextCorner] = FacePlacement{ .face = targetFace, .rotation = faceRotation, }; - if (animState->currentConfigIndex >= 0) { - const auto &config = kGiftAnimations[animState->currentConfigIndex]; + if (state->currentPhaseIndex >= 0) { + const auto &config = kGiftAnimations[state->currentPhaseIndex]; const auto initial = Rotation{ - animState->initialRotationX, - animState->initialRotationY, + state->initialRotationX, + state->initialRotationY, }; const auto endRotation = config.rotation(initial, config.duration); @@ -882,68 +1013,69 @@ void StartGiftFlightToFace( endRotation.y, targetFace); if (targetCorners) { - animState->flightTargetCorners = RotateCornersForFace( + state->flightTargetCorners = RotateCornersForFace( *targetCorners, faceRotation); } else { - animState->flightTargetCorners = std::nullopt; + state->flightTargetCorners = std::nullopt; } } else { - animState->flightTargetCorners = std::nullopt; + state->flightTargetCorners = std::nullopt; } - animState->flightAnimation.start( - [=] { canvas->update(); }, - 0., - 1., - kFlightDuration, - anim::easeInCubic); + state->flightAnimation.start([=] { + canvas->update(); + }, 0., 1., kFlightDuration, anim::easeInCubic); - if (!animState->continuousAnimation.animating()) { - animState->continuousAnimation.start(); + if (!state->continuousAnimation.animating()) { + state->continuousAnimation.start(); } } -void LandCurrentGift(CraftAnimationState *animState, crl::time now) { - if (animState->currentlyFlying < 0) { +void LandCurrentGift(not_null state, crl::time now) { + if (state->currentlyFlying < 0) { return; } - const auto giftNumber = ++animState->giftsLanded; + const auto giftNumber = ++state->giftsLanded; const auto isLastGift - = animState->allGiftsLanded - = (giftNumber >= animState->totalGifts); + = state->allGiftsLanded + = (giftNumber >= state->totalGifts); const auto configIndex = (giftNumber - 1) * 2 + (isLastGift ? 0 : 1); Assert(configIndex < 7); const auto &config = kGiftAnimations[configIndex]; - animState->currentlyFlying = -1; + state->currentlyFlying = -1; - animState->initialRotationX = animState->rotationX; - animState->initialRotationY = animState->rotationY; - animState->currentConfigIndex = configIndex; - animState->animationStartTime = now; - animState->nextFaceIndex = config.nextFaceIndex; - animState->nextFaceRotation = config.nextFaceRotation; - animState->nextFaceRevealed = false; + state->initialRotationX = state->rotationX; + state->initialRotationY = state->rotationY; + state->currentPhaseIndex = configIndex; + state->currentPhaseFinished = false; + state->animationStartTime = now; + state->nextFaceIndex = config.targetFaceIndex; + state->nextFaceRotation = config.targetFaceRotation; + state->nextFaceRevealed = false; } -void PaintLoadingAnimation(QPainter &p, CraftAnimationState *animState) { - const auto &loading = animState->loadingAnimation; - if (!loading || !animState->loadingStartedTime) { +void PaintLoadingAnimation( + QPainter &p, + not_null state) { + const auto &loading = state->loadingAnimation; + if (!loading || !state->loadingStartedTime) { return; } - const auto loadingShown = animState->loadingShownAnimation.value( - animState->loadingFadingOut ? 0. : 1.); + const auto loadingShown = state->loadingShownAnimation.value( + state->loadingFadingOut ? 0. : 1.); if (loadingShown <= 0.) { return; } - const auto shared = animState->shared; - const auto forge = shared->forgeRect.translated(0, shared->craftingOffsetY); + const auto shared = state->shared.get(); + const auto forge = shared->forgeRect.translated( + { 0, shared->craftingOffsetY }); const auto inner = forge.marginsRemoved({ st::craftForgePadding, st::craftForgePadding, @@ -951,11 +1083,11 @@ void PaintLoadingAnimation(QPainter &p, CraftAnimationState *animState) { st::craftForgePadding, }); - const auto state = loading->computeState(); + const auto radial = loading->computeState(); const auto adjustedState = RadialState{ - .shown = state.shown * loadingShown, - .arcFrom = state.arcFrom, - .arcLength = state.arcLength, + .shown = radial.shown * loadingShown, + .arcFrom = radial.arcFrom, + .arcLength = radial.arcLength, }; auto pen = QPen(st::white->c); @@ -986,7 +1118,11 @@ QImage CraftState::CornerSnapshot::gift(float64 progress) const { return giftFrame; } -void CraftState::paint(QPainter &p, QSize size, int craftingHeight, float64 slideProgress) { +void CraftState::paint( + QPainter &p, + QSize size, + int craftingHeight, + float64 slideProgress) { const auto width = size.width(); const auto getBackdrop = [&](BackdropView &backdrop) { const auto ratio = style::DevicePixelRatio(); @@ -998,8 +1134,11 @@ void CraftState::paint(QPainter &p, QSize size, int craftingHeight, float64 slid return gradient; }; auto patternOffsetY = 0.; - if (slideProgress > 0. && containerHeight > 0 && craftingAreaCenterY > 0) { - patternOffsetY = (containerHeight / 2. - craftingAreaCenterY) * slideProgress; + if (slideProgress > 0. + && containerHeight > 0 + && craftingAreaCenterY > 0) { + patternOffsetY = (containerHeight / 2. - craftingAreaCenterY) + * slideProgress; } const auto paintPattern = [&]( QPainter &q, @@ -1118,34 +1257,40 @@ CraftState::EmptySide CraftState::prepareEmptySide(int index) const { void StartCraftAnimation( not_null container, - std::shared_ptr state, - Fn startRequest) { + std::shared_ptr shared, + Fn startRequest, + Fn closeOnFail) { while (container->count() > 0) { delete container->widgetAt(0); } - - const auto height = state->containerHeight; - const auto craftingHeight = state->craftingBottom - state->craftingTop; - auto canvas = object_ptr(container); const auto raw = canvas.data(); + const auto state = raw->lifetime().make_state(); + + const auto height = shared->containerHeight; + const auto craftingHeight = shared->craftingBottom - shared->craftingTop; + state->shared = std::move(shared); + raw->resize(container->width(), height); - const auto animState = raw->lifetime().make_state(); - animState->shared = std::move(state); - for (auto &corner : animState->shared->corners) { + for (auto &corner : state->shared->corners) { if (corner.giftButton) { - ++animState->totalGifts; + ++state->totalGifts; } } - animState->loadingAnimation = std::make_unique( + state->loadingAnimation = std::make_unique( [=] { raw->update(); }, st::craftForgeLoading); raw->paintOn([=](QPainter &p) { - const auto shared = animState->shared; - const auto slideProgress = animState->slideOutAnimation.value(1.); + if (state->failureAnimation + && state->failureAnimation->playerFinished) { + crl::on_main(closeOnFail); + } + + const auto shared = state->shared.get(); + const auto slideProgress = state->slideOutAnimation.value(1.); shared->paint(p, raw->size(), craftingHeight, slideProgress); shared->craftingOffsetY = (shared->containerHeight / 2.) @@ -1161,43 +1306,52 @@ void StartCraftAnimation( for (auto i = 0; i < 4; ++i) { const auto &corner = shared->corners[i]; - if (corner.giftButton && animState->giftToSide[i].face < 0) { - const auto giftTopLeft = QPointF(corner.originalRect.topLeft()) + if (corner.giftButton && state->giftToSide[i].face < 0) { + const auto giftTopLeft = QPointF() + + QPointF(corner.originalRect.topLeft()) + QPointF(0, shared->craftingOffsetY); p.drawImage(giftTopLeft, corner.gift(0)); } } - const auto flying = animState->currentlyFlying; + const auto flying = state->currentlyFlying; const auto firstFlyProgress = (flying == 0) - ? animState->flightAnimation.value(1.) - : animState->giftsLanded + ? state->flightAnimation.value(1.) + : state->giftsLanded ? 1. : 0.; - const auto loadingVisible = animState->loadingStartedTime - && !animState->loadingFadingOut; + const auto loadingShown = state->loadingStartedTime + && !state->loadingFadingOut; if (firstFlyProgress < 1.) { - const auto skipForgeIcon = loadingVisible && anim::Disabled(); - PaintCubeFirstFlight(p, *animState, firstFlyProgress, skipForgeIcon); + const auto skipForgeIcon = loadingShown && anim::Disabled(); + PaintCubeFirstFlight( + p, + state, + firstFlyProgress, + skipForgeIcon); } else { - PaintCube(p, animState, cubeCenter, cubeSize); + PaintCube(p, state, raw, cubeCenter, cubeSize); } if (flying >= 0) { const auto &corner = shared->corners[flying]; const auto progress = (flying > 0) - ? animState->flightAnimation.value(1.) + ? state->flightAnimation.value(1.) : firstFlyProgress; - if (animState->flightTargetCorners) { + if (state->flightTargetCorners) { const auto sourceRect = QRectF(corner.originalRect) .translated(0, shared->craftingOffsetY); const auto sourceCorners = RectToCorners(sourceRect); const auto interpolatedCorners = InterpolateQuadCorners( sourceCorners, - *animState->flightTargetCorners, + *state->flightTargetCorners, + progress); + PaintFlyingGiftWithQuad( + p, + corner, + interpolatedCorners, progress); - PaintFlyingGiftWithQuad(p, corner, interpolatedCorners, progress); } else { const auto position = ComputeGiftFlightPosition( corner.originalRect, @@ -1209,35 +1363,35 @@ void StartCraftAnimation( } } - PaintLoadingAnimation(p, animState); + PaintLoadingAnimation(p, state); } }); const auto startFlying = [=] { - if (animState->loadingStartedTime > 0) { - animState->loadingFadingOut = true; - animState->loadingShownAnimation.start( + if (state->loadingStartedTime > 0) { + state->loadingFadingOut = true; + state->loadingShownAnimation.start( [=] { raw->update(); }, 1., 0., kLoadingFadeOutDuration); } - StartGiftFlight(animState, raw, 0); + StartGiftFlight(state, raw, 0); }; const auto tryStartFlying = [=] { - const auto slideFinished = !animState->slideOutAnimation.animating(); - const auto resultArrived = animState->craftResult.has_value(); - const auto notYetFlying = (animState->currentlyFlying < 0) - && (animState->giftsLanded == 0); + const auto slideFinished = !state->slideOutAnimation.animating(); + const auto resultArrived = state->craftResult.has_value(); + const auto notYetFlying = (state->currentlyFlying < 0) + && (state->giftsLanded == 0); if (!slideFinished || !notYetFlying) { return; } if (!resultArrived) { - if (!animState->loadingStartedTime) { - animState->loadingStartedTime = crl::now(); - animState->loadingAnimation->start(); - animState->loadingShownAnimation.start( + if (!state->loadingStartedTime) { + state->loadingStartedTime = crl::now(); + state->loadingAnimation->start(); + state->loadingShownAnimation.start( [=] { raw->update(); }, 0., 1., @@ -1245,88 +1399,92 @@ void StartCraftAnimation( } return; } - if (!animState->loadingStartedTime) { + if (!state->loadingStartedTime) { startFlying(); return; } - const auto elapsed = crl::now() - animState->loadingStartedTime; + const auto elapsed = crl::now() - state->loadingStartedTime; if (elapsed >= kLoadingMinDuration) { startFlying(); } else { - base::call_delayed(kLoadingMinDuration - elapsed, raw, startFlying); + base::call_delayed( + kLoadingMinDuration - elapsed, + raw, + startFlying); } }; - animState->slideOutAnimation.start( - [=] { - raw->update(); + state->slideOutAnimation.start([=] { + raw->update(); - const auto progress = animState->slideOutAnimation.value(1.); - if (progress >= 1.) { - tryStartFlying(); - } - }, - 0., - 1., - crl::time(300), - anim::easeOutCubic); + const auto progress = state->slideOutAnimation.value(1.); + if (progress >= 1.) { + tryStartFlying(); + } + }, 0., 1., crl::time(300), anim::easeOutCubic); - animState->continuousAnimation.init([=](crl::time now) { - if (animState->currentConfigIndex >= 0) { - const auto &config = kGiftAnimations[animState->currentConfigIndex]; - const auto elapsed = (now - animState->animationStartTime) + state->continuousAnimation.init([=](crl::time now) { + if (state->currentPhaseIndex >= 0) { + const auto &config = kGiftAnimations[state->currentPhaseIndex]; + const auto elapsed = (now - state->animationStartTime) / anim::SlowMultiplier(); const auto initial = Rotation{ - animState->initialRotationX, - animState->initialRotationY, + state->initialRotationX, + state->initialRotationY, }; const auto r = config.rotation(initial, elapsed); - animState->rotationX = r.x; - animState->rotationY = r.y; + state->rotationX = r.x; + state->rotationY = r.y; } raw->update(); - if (animState->currentlyFlying >= 0 - && !animState->flightAnimation.animating()) { - LandCurrentGift(animState, now); + if (state->currentlyFlying >= 0 + && !state->flightAnimation.animating()) { + LandCurrentGift(state, now); } - if (!animState->flightAnimation.animating() - && animState->currentlyFlying < 0 - && animState->giftsLanded > 0 - && animState->giftsLanded < animState->totalGifts - && animState->currentConfigIndex >= 0) { + if (!state->flightAnimation.animating() + && state->currentlyFlying < 0 + && state->giftsLanded > 0 + && state->giftsLanded < state->totalGifts + && state->currentPhaseIndex >= 0) { - const auto &config = kGiftAnimations[animState->currentConfigIndex]; - const auto elapsed = (now - animState->animationStartTime) + const auto &config = kGiftAnimations[state->currentPhaseIndex]; + const auto elapsed = (now - state->animationStartTime) / anim::SlowMultiplier(); const auto flightStartTime = config.duration - kFlightDuration; if (elapsed >= flightStartTime) { - StartGiftFlightToFace(animState, raw, animState->nextFaceIndex); + StartGiftFlightToFace(state, raw, state->nextFaceIndex); } } - if (!animState->flightAnimation.animating() - && animState->currentlyFlying < 0 - && animState->giftsLanded >= animState->totalGifts - && animState->totalGifts > 0 - && animState->currentConfigIndex >= 0) { + if (!state->currentPhaseFinished + && !state->flightAnimation.animating() + && state->currentlyFlying < 0 + && state->giftsLanded >= state->totalGifts + && state->totalGifts > 0 + && state->currentPhaseIndex >= 0) { - const auto &config = kGiftAnimations[animState->currentConfigIndex]; - const auto elapsed = (now - animState->animationStartTime) + const auto &config = kGiftAnimations[state->currentPhaseIndex]; + const auto elapsed = (now - state->animationStartTime) / anim::SlowMultiplier(); if (elapsed >= config.duration) { - animState->currentConfigIndex = -1; + state->currentPhaseFinished = true; } } - const auto hasMoreFlights = (animState->giftsLanded < animState->totalGifts); - const auto isFlying = animState->flightAnimation.animating(); - const auto isAnimating = (animState->currentConfigIndex >= 0); - const auto isLoadingShown = animState->loadingShownAnimation.animating(); + const auto hasMoreFlights = (state->giftsLanded < state->totalGifts); + const auto isFlying = state->flightAnimation.animating(); + const auto isAnimating = !state->currentPhaseFinished; + const auto isLoadingShown = state->loadingShownAnimation.animating(); + const auto isFailureAnimation = state->failureAnimation != nullptr; - return hasMoreFlights || isFlying || isAnimating || isLoadingShown; + return hasMoreFlights + || isFlying + || isAnimating + || isLoadingShown + || isFailureAnimation; }); container->add(std::move(canvas)); @@ -1337,7 +1495,7 @@ void StartCraftAnimation( if (!weak) { return; } - animState->craftResult = std::move(result); + state->craftResult = std::move(result); tryStartFlying(); }); } diff --git a/Telegram/SourceFiles/boxes/star_gift_craft_animation.h b/Telegram/SourceFiles/boxes/star_gift_craft_animation.h index 4aab84a72b..7c74e1956c 100644 --- a/Telegram/SourceFiles/boxes/star_gift_craft_animation.h +++ b/Telegram/SourceFiles/boxes/star_gift_craft_animation.h @@ -13,6 +13,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/radial_animation.h" #include "ui/text/text_custom_emoji.h" +namespace Main { +class Session; +} // namespace Main + +namespace HistoryView { +class StickerPlayer; +} // namespace HistoryView + namespace Info::PeerGifts { class GiftButton; } // namespace Info::PeerGifts @@ -26,13 +34,11 @@ using CraftResultCallback = Fn)>; struct BackdropView { Data::UniqueGiftBackdrop colors; QImage gradient; - }; struct PatternView { std::unique_ptr emoji; base::flat_map> emojis; - }; struct CraftState { @@ -84,6 +90,8 @@ struct CraftState { std::array forgeSides; EmptySide finalSide; + Main::Session *session = nullptr; + int containerHeight = 0; int craftingTop = 0; int craftingBottom = 0; @@ -93,13 +101,22 @@ struct CraftState { void paint(QPainter &p, QSize size, int craftingHeight, float64 slideProgress = 0.); void updateForGiftCount(int count); [[nodiscard]] EmptySide prepareEmptySide(int index) const; - }; struct FacePlacement { int face = -1; int rotation = 0; +}; +struct FailureAnimation { + std::unique_ptr player; + QImage frame; + DocumentData *document = nullptr; + Animations::Simple scaleAnimation; + bool scaleStarted = false; + bool playerStarted = false; + bool playerFinished = false; + rpl::lifetime lifetime; }; struct CraftAnimationState { @@ -115,10 +132,11 @@ struct CraftAnimationState { int giftsLanded = 0; int totalGifts = 0; bool allGiftsLanded = false; + bool currentPhaseFinished = false; std::array giftToSide; Animations::Simple flightAnimation; - int currentConfigIndex = -1; + int currentPhaseIndex = -1; crl::time animationStartTime = 0; float64 initialRotationX = 0.; float64 initialRotationY = 0.; @@ -134,11 +152,13 @@ struct CraftAnimationState { crl::time loadingStartedTime = 0; bool loadingFadingOut = false; + std::unique_ptr failureAnimation; }; void StartCraftAnimation( not_null container, std::shared_ptr state, - Fn startRequest); + Fn startRequest, + Fn closeOnFail); } // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp b/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp index 5c0bc8d34e..cc11111699 100644 --- a/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp @@ -804,9 +804,10 @@ void ShowSelectGiftBox( } void Craft( - not_null container, + not_null box, std::shared_ptr state, - const std::vector &gifts) { + const std::vector &gifts, + Fn closeParent) { auto startRequest = [=](CraftResultCallback done) { constexpr auto kDelays = std::array{ 100, 200, 300, 400, 500, 1000, 2000 @@ -814,10 +815,11 @@ void Craft( const auto delay = kDelays[base::RandomIndex(kDelays.size())]; const auto giftsCopy = gifts; - base::call_delayed(delay, container, [=] { - const auto shouldSucceed = (base::RandomIndex(5) != 0); - if (shouldSucceed && !giftsCopy.empty()) { - const auto &chosen = giftsCopy[base::RandomIndex(giftsCopy.size())]; + base::call_delayed(delay, box, [=] { + const auto shouldSucceed = true;// (base::RandomIndex(5) != 0); + const auto count = int(giftsCopy.size()); + if (shouldSucceed && !count) { + const auto &chosen = giftsCopy[base::RandomIndex(count)]; auto info = Data::StarGift{ .id = chosen.unique->initialGiftId, .unique = chosen.unique, @@ -834,14 +836,31 @@ void Craft( } }); }; - StartCraftAnimation(container, std::move(state), std::move(startRequest)); + + const auto container = box->verticalLayout(); + const auto show = box->uiShow(); + const auto closeOnFail = crl::guard(box, [=] { + if (const auto onstack = closeParent) { + onstack(); + } + box->closeBox(); + show->showToast(tr::lng_gift_craft_failed(tr::now)); + }); + StartCraftAnimation( + container, + std::move(state), + std::move(startRequest), + closeOnFail); } void MakeCraftContent( not_null box, not_null controller, std::vector gifts, + Fn closeParent, bool autoStartCraft) { + Expects(!gifts.empty()); + struct State { std::shared_ptr craftState; GradientButton *button = nullptr; @@ -861,9 +880,11 @@ void MakeCraftContent( bool crafting = false; }; const auto session = &controller->session(); + const auto initialGiftId = gifts.front().unique->initialGiftId; const auto state = box->lifetime().make_state(); state->craftState = std::make_shared(); + state->craftState->session = session; state->craftState->coversAnimate = true; { @@ -935,7 +956,6 @@ void MakeCraftContent( } }, raw->lifetime()); - const auto initialGiftId = state->chosen.current().front().unique->initialGiftId; std::move( crafting.editRequests ) | rpl::on_next([=](int index) { @@ -1071,7 +1091,7 @@ void MakeCraftContent( const auto renderWidget = [&](QWidget *widget) { const auto pos = widget->pos() - bottomRect.topLeft(); - widget->render(&bottomPart, pos, QRegion(), QWidget::DrawChildren); + widget->render(&bottomPart, pos, {}, QWidget::DrawChildren); }; renderWidget(state->about); renderWidget(state->attributes); @@ -1081,7 +1101,7 @@ void MakeCraftContent( cs->bottomPartY = aboutPos.y(); cs->containerHeight = raw->height(); - Craft(raw, state->craftState, state->chosen.current()); + Craft(box, state->craftState, state->chosen.current(), closeParent); }; button->setClickedCallback(startCrafting); @@ -1113,12 +1133,18 @@ void MakeCraftContent( void ShowGiftCraftBoxInternal( not_null controller, std::vector gifts, + Fn closeParent, bool autoStartCraft) { controller->show(Box([=](not_null box) { box->setStyle(st::giftCraftBox); box->setWidth(st::boxWidth); box->setNoContentMargin(true); - MakeCraftContent(box, controller, gifts, autoStartCraft); + MakeCraftContent( + box, + controller, + gifts, + closeParent, + autoStartCraft); AddUniqueCloseButton(box); #if _DEBUG @@ -1132,7 +1158,7 @@ void ShowGiftCraftBoxInternal( } else { copy = full; } - ShowGiftCraftBoxInternal(controller, copy, true); + ShowGiftCraftBoxInternal(controller, copy, [] {}, true); }); }, box->lifetime()); } @@ -1145,7 +1171,8 @@ void ShowGiftCraftBoxInternal( void ShowGiftCraftInfoBox( not_null controller, std::shared_ptr gift, - Data::SavedStarGiftId savedId) { + Data::SavedStarGiftId savedId, + Fn closeParent) { controller->show(Box([=](not_null box) { const auto container = box->verticalLayout(); auto cover = tr::lng_gift_craft_info_title( @@ -1189,10 +1216,15 @@ void ShowGiftCraftInfoBox( box->setNoContentMargin(true); box->addButton(tr::lng_gift_craft_start_button(), [=] { + const auto weak = base::make_weak(box); ShowGiftCraftBoxInternal( controller, { GiftForCraft{ gift, savedId } }, + closeParent, false); + if (const auto strong = box.get()) { + strong->closeBox(); + } }); })); } @@ -1208,7 +1240,7 @@ void ShowTestGiftCraftBox( entry.manageId = std::move(gift.manageId); converted.push_back(std::move(entry)); } - ShowGiftCraftBoxInternal(controller, std::move(converted), true); + ShowGiftCraftBoxInternal(controller, std::move(converted), [] {}, true); } } // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_craft_box.h b/Telegram/SourceFiles/boxes/star_gift_craft_box.h index 2ff7e17188..ab455df736 100644 --- a/Telegram/SourceFiles/boxes/star_gift_craft_box.h +++ b/Telegram/SourceFiles/boxes/star_gift_craft_box.h @@ -27,7 +27,8 @@ struct GiftForCraftEntry { void ShowGiftCraftInfoBox( not_null controller, std::shared_ptr gift, - Data::SavedStarGiftId savedId); + Data::SavedStarGiftId savedId, + Fn closeParent); void ShowTestGiftCraftBox( not_null controller, diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index 2645ba31f5..9a19f6c579 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -1579,7 +1579,11 @@ void GenericCreditsEntryBody( const auto unique = e.uniqueGift; const auto savedId = EntryToSavedStarGiftId(&show->session(), e); if (const auto window = show->resolveWindow()) { - Ui::ShowGiftCraftInfoBox(window, unique, savedId); + const auto closeParent = crl::guard(box, [=] { + box->closeBox(); + }); + using namespace Ui; + ShowGiftCraftInfoBox(window, unique, savedId, closeParent); } } : Fn(); AddUniqueCloseMoreButton(box, st, [=](not_null menu) { diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index bdf47943ef..176806ce7c 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -1685,7 +1685,9 @@ SessionController::SessionController( #if _DEBUG // TEST: Auto-open craft box on startup constexpr auto kGiftsCount = 4; crl::on_main(this, [=] { - return; + if (rand() % 2 == 0) { + return; + } const auto user = session->user(); session->api().request(MTPpayments_GetSavedStarGifts( MTP_flags(MTPpayments_GetSavedStarGifts::Flag::f_exclude_unlimited),