diff --git a/Telegram/Resources/animations/chat/video_to_voice.tgs b/Telegram/Resources/animations/chat/video_to_voice.tgs new file mode 100644 index 0000000000..854a6c734c Binary files /dev/null and b/Telegram/Resources/animations/chat/video_to_voice.tgs differ diff --git a/Telegram/Resources/animations/chat/voice_to_video.tgs b/Telegram/Resources/animations/chat/voice_to_video.tgs new file mode 100644 index 0000000000..de864f07d9 Binary files /dev/null and b/Telegram/Resources/animations/chat/voice_to_video.tgs differ diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index a33a9ff016..3902bfff8b 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -17,6 +17,8 @@ ../../animations/stats_earn.tgs ../../animations/voice_ttl_idle.tgs ../../animations/voice_ttl_start.tgs + ../../animations/chat/voice_to_video.tgs + ../../animations/chat/video_to_voice.tgs ../../animations/palette.tgs ../../animations/sleep.tgs ../../animations/greeting.tgs diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 08b4c44f3f..e1b1db8b3b 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -153,10 +153,7 @@ SendButton { sendIconFillPadding: pixels; inner: IconButton; stars: RoundButton; - record: icon; - recordOver: icon; - round: icon; - roundOver: icon; + recordSize: size; sendDisabledFg: color; } @@ -1287,18 +1284,11 @@ historyRecordVoiceFgOver: historyComposeIconFgOver; historyRecordVoiceFgInactive: attentionButtonFg; historyRecordVoiceFgActive: windowBgActive; historyRecordVoiceFgActiveIcon: windowFgActive; -historyRecordVoice: icon {{ "chat/input_record", historyRecordVoiceFg }}; -historyRecordVoiceOver: icon {{ "chat/input_record", historyRecordVoiceFgOver }}; historyRecordVoiceOnceBg: icon {{ "voice_lock/audio_once_bg", historySendIconFg }}; historyRecordVoiceOnceBgOver: icon {{ "voice_lock/audio_once_bg", historySendIconFgOver }}; historyRecordVoiceOnceFg: icon {{ "voice_lock/audio_once_number", windowFgActive }}; historyRecordVoiceOnceFgOver: icon {{ "voice_lock/audio_once_number", windowFgActive }}; historyRecordVoiceOnceInactive: icon {{ "chat/audio_once", windowSubTextFg }}; -historyRecordVoiceActive: icon {{ "chat/input_record_filled", historyRecordVoiceFgActiveIcon }}; -historyRecordRound: icon {{ "chat/input_video", historyRecordVoiceFg }}; -historyRecordRoundOver: icon {{ "chat/input_video", historyRecordVoiceFgOver }}; -historyRecordRoundActive: icon {{ "chat/input_video", historyRecordVoiceFgActiveIcon }}; -historyRecordRoundIconPosition: point(0px, 0px); historyRecordSendIconPosition: point(2px, 0px); historyRecordVoiceRippleBgActive: lightButtonBgOver; historyRecordSignalRadius: 5px; @@ -1411,12 +1401,10 @@ historySend: SendButton { textTop: 5px; width: -8px; } - record: historyRecordVoice; - recordOver: historyRecordVoiceOver; - round: historyRecordRound; - roundOver: historyRecordRoundOver; + recordSize: size(26px, 26px); sendDisabledFg: historyComposeIconFg; } +historyRecordFrameIndex: 30; defaultComposeFilesMenu: IconButton(defaultIconButton) { width: 48px; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp index b796d27858..abca0b803a 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/controls/history_view_voice_record_button.h" +#include "lottie/lottie_icon.h" #include "ui/paint/blobs.h" #include "ui/painter.h" #include "styles/style_chat.h" @@ -24,6 +25,8 @@ constexpr auto kBlobAlpha = 76. / 255.; constexpr auto kBlobMaxSpeed = 5.0; constexpr auto kLevelDuration = 100. + 500. * 0.33; constexpr auto kBlobsScaleEnterDuration = crl::time(250); +constexpr auto kVoiceIconIndex = 0; +constexpr auto kRoundIconIndex = 1; auto Blobs() { return std::vector{ @@ -135,22 +138,28 @@ void VoiceRecordButton::init() { p.scale(scale, scale); } const auto state = *currentState; - const auto icon = (state == Type::Send) - ? st::historySendIcon - : (state == Type::Record) - ? st::historyRecordVoiceActive - : st::historyRecordRoundActive; - const auto position = (state == Type::Send) - ? st::historyRecordSendIconPosition - : (state == Type::Record) - ? QPoint(0, 0) - : st::historyRecordRoundIconPosition; - icon.paint( - p, - -icon.width() / 2 + position.x(), - -icon.height() / 2 + position.y(), - 0, - st::historyRecordVoiceFgActiveIcon->c); + if (state == Type::Send) { + const auto icon = st::historySendIcon; + const auto position = st::historyRecordSendIconPosition; + icon.paint( + p, + -icon.width() / 2 + position.x(), + -icon.height() / 2 + position.y(), + 0, + st::historyRecordVoiceFgActiveIcon->c); + } else { + const auto index = (state == Type::Record) + ? kVoiceIconIndex + : kRoundIconIndex; + auto &icon = _voiceRoundIcons[index]; + if (!icon) { + initVoiceRoundIcon(index); + } + icon->paintInCenter( + p, + rect().translated(-_center, -_center), + st::historyRecordVoiceFgActiveIcon->c); + } } }, lifetime()); @@ -205,6 +214,18 @@ void VoiceRecordButton::init() { }, lifetime()); } +void VoiceRecordButton::initVoiceRoundIcon(int index) { + Expects(index >= 0 && index < 2); + + _voiceRoundIcons[index] = Lottie::MakeIcon({ + .path = ((index == kVoiceIconIndex) + ? u":/animations/chat/voice_to_video.tgs"_q + : u":/animations/chat/video_to_voice.tgs"_q), + .sizeOverride = st::historySend.recordSize, + .colorizeUsingAlpha = true, + }); +} + rpl::producer VoiceRecordButton::actives() const { return events( ) | rpl::filter([=](not_null e) { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h index e8e2dd7b65..4465a95925 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h @@ -15,6 +15,10 @@ namespace style { struct RecordBar; } // namespace style +namespace Lottie { +class Icon; +} // namespace Lottie + namespace Ui::Paint { class Blobs; } // namespace Ui::Paint @@ -47,8 +51,10 @@ public: private: void init(); + void initVoiceRoundIcon(int index); std::unique_ptr _blobs; + std::array, 2> _voiceRoundIcons; crl::time _lastUpdateTime = 0; crl::time _blobsHideLastTime = 0; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index fa3d245c58..442277256c 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -493,10 +493,6 @@ storiesLike: IconButton(storiesAttach) { icon: icon {{ "chat/input_like", storiesComposeGrayIcon }}; iconOver: icon {{ "chat/input_like", storiesComposeGrayIcon }}; } -storiesRecordVoice: icon {{ "chat/input_record", storiesComposeGrayIcon }}; -storiesRecordVoiceOver: icon {{ "chat/input_record", storiesComposeGrayIcon }}; -storiesRecordRound: icon {{ "chat/input_video", storiesComposeGrayIcon }}; -storiesRecordRoundOver: icon {{ "chat/input_video", storiesComposeGrayIcon }}; storiesEditStars: IconButton(storiesAttach) { icon: icon{{ "chat/input_paid", storiesComposeGrayIcon }}; iconOver: icon{{ "chat/input_paid", storiesComposeGrayIcon }}; @@ -727,10 +723,6 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { rippleAreaSize: 40px; rippleAreaPosition: point(1px, 1px); } - record: storiesRecordVoice; - recordOver: storiesRecordVoiceOver; - round: storiesRecordRound; - roundOver: storiesRecordRoundOver; sendDisabledFg: storiesComposeGrayText; } attach: storiesAttach; diff --git a/Telegram/SourceFiles/ui/controls/send_button.cpp b/Telegram/SourceFiles/ui/controls/send_button.cpp index 7cf772cc2d..0dd88a0f57 100644 --- a/Telegram/SourceFiles/ui/controls/send_button.cpp +++ b/Telegram/SourceFiles/ui/controls/send_button.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/send_button.h" #include "lang/lang_tag.h" +#include "lottie/lottie_icon.h" #include "ui/effects/ripple_animation.h" #include "ui/text/text_utilities.h" #include "ui/painter.h" @@ -19,7 +20,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { namespace { -constexpr int kWideScale = 5; +constexpr auto kWideScale = 5; +constexpr auto kVoiceToRoundIndex = 0; +constexpr auto kRoundToVoiceIndex = 1; } // namespace @@ -29,19 +32,34 @@ SendButton::SendButton(QWidget *parent, const style::SendButton &st) updateSize(); } +SendButton::~SendButton() = default; + void SendButton::setState(State state) { if (_state == state) { return; } + + const auto previousType = _state.type; + const auto newType = state.type; + const auto voiceRoundTransition = isVoiceRoundTransition( + previousType, + newType); + const auto hasSlowmode = (_state.slowmodeDelay > 0); const auto hasSlowmodeChanged = hasSlowmode != (state.slowmodeDelay > 0); auto withSameSlowmode = state; withSameSlowmode.slowmodeDelay = _state.slowmodeDelay; const auto animate = hasSlowmodeChanged || (!hasSlowmode && withSameSlowmode != _state); - if (animate) { + + if (animate && !voiceRoundTransition) { _contentFrom = grabContent(); } + + if (_voiceRoundAnimating && !voiceRoundTransition) { + _voiceRoundAnimating = false; + } + if (_state.slowmodeDelay != state.slowmodeDelay) { const auto seconds = state.slowmodeDelay; const auto minutes = seconds / 60; @@ -60,7 +78,29 @@ void SendButton::setState(State state) { kMarkupTextOptions); } _state = state; - if (animate) { + + if (voiceRoundTransition) { + _voiceRoundAnimating = true; + + const auto toRound = (newType == Type::Round); + const auto index = toRound ? kVoiceToRoundIndex : kRoundToVoiceIndex; + auto &icon = _voiceRoundIcons[index]; + if (!icon) { + initVoiceRoundIcon(index); + } + icon->animate([=, raw = icon.get()] { + update(); + if (!raw->animating()) { + _voiceRoundAnimating = false; + } + }, 0, icon->framesCount() - 1); + auto &after = _voiceRoundIcons[1 - index]; + if (!after) { + initVoiceRoundIcon(1 - index); + } else if (after->frameIndex() != 0) { + after->jumpTo(0, nullptr); + } + } else if (animate) { _stateChangeFromWidth = width(); _stateChangeAnimation.stop(); updateSize(); @@ -86,6 +126,12 @@ void SendButton::paintEvent(QPaintEvent *e) { auto p = QPainter(this); auto over = (isDown() || isOver()); + + if (_voiceRoundAnimating) { + paintVoiceRoundIcon(p, over); + return; + } + auto changed = _stateChangeAnimation.value(1.); if (changed < 1.) { PainterHighQualityEnabler hq(p); @@ -153,11 +199,7 @@ void SendButton::paintRecord(QPainter &p, bool over) { (width() - _st.inner.rippleAreaSize) / 2, _st.inner.rippleAreaPosition.y()); } - - const auto &icon = (isDisabled() || !over) - ? _st.record - : _st.recordOver; - icon.paintInCenter(p, rect()); + paintLottieIcon(p, kVoiceToRoundIndex, over); } void SendButton::paintRound(QPainter &p, bool over) { @@ -167,11 +209,20 @@ void SendButton::paintRound(QPainter &p, bool over) { (width() - _st.inner.rippleAreaSize) / 2, _st.inner.rippleAreaPosition.y()); } + paintLottieIcon(p, kRoundToVoiceIndex, over); +} - const auto &icon = (isDisabled() || !over) - ? _st.round - : _st.roundOver; - icon.paintInCenter(p, rect()); +void SendButton::paintLottieIcon(QPainter &p, int index, bool over) { + auto &icon = _voiceRoundIcons[index]; + if (!icon) { + initVoiceRoundIcon(index); + } else if (!_voiceRoundAnimating && icon->frameIndex() != 0) { + icon->jumpTo(0, [=] { update(); }); + } + const auto color = (isDisabled() || !over) + ? st::historyRecordVoiceFg->c + : st::historyRecordVoiceFgOver->c; + icon->paintInCenter(p, rect(), color); } void SendButton::paintSave(QPainter &p, bool over) { @@ -330,6 +381,39 @@ QPoint SendButton::prepareRippleStartPosition() const { return real - QPoint((width() - size) / 2, y); } +void SendButton::initVoiceRoundIcon(int index) { + Expects(index >= 0 && index < 2); + + _voiceRoundIcons[index] = Lottie::MakeIcon({ + .path = ((index == kVoiceToRoundIndex) + ? u":/animations/chat/voice_to_video.tgs"_q + : u":/animations/chat/video_to_voice.tgs"_q), + .sizeOverride = _st.recordSize, + .colorizeUsingAlpha = true, + }); +} + +void SendButton::paintVoiceRoundIcon(QPainter &p, bool over) { + if (!isDisabled()) { + paintRipple( + p, + (width() - _st.inner.rippleAreaSize) / 2, + _st.inner.rippleAreaPosition.y()); + } + + const auto color = (isDisabled() || !over) + ? st::historyRecordVoiceFg->c + : st::historyRecordVoiceFgOver->c; + const auto toVideo = (_state.type == Type::Round); + const auto index = toVideo ? kVoiceToRoundIndex : kRoundToVoiceIndex; + _voiceRoundIcons[index]->paintInCenter(p, rect(), color); +} + +bool SendButton::isVoiceRoundTransition(Type from, Type to) { + return (from == Type::Record && to == Type::Round) + || (from == Type::Round && to == Type::Record); +} + SendStarButton::SendStarButton( QWidget *parent, const style::IconButton &st, diff --git a/Telegram/SourceFiles/ui/controls/send_button.h b/Telegram/SourceFiles/ui/controls/send_button.h index cd9fe21b72..04529b9b95 100644 --- a/Telegram/SourceFiles/ui/controls/send_button.h +++ b/Telegram/SourceFiles/ui/controls/send_button.h @@ -9,6 +9,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/buttons.h" +#include + +namespace Lottie { +class Icon; +} // namespace Lottie + namespace style { struct SendButton; struct IconButton; @@ -20,6 +26,7 @@ namespace Ui { class SendButton final : public RippleButton { public: SendButton(QWidget *parent, const style::SendButton &st); + ~SendButton(); static constexpr auto kSlowmodeDelayLimit = 100 * 60; @@ -76,6 +83,11 @@ private: void paintSlowmode(QPainter &p); void paintStarsToSend(QPainter &p, bool over); + void initVoiceRoundIcon(int index); + void paintVoiceRoundIcon(QPainter &p, bool over); + [[nodiscard]] static bool isVoiceRoundTransition(Type from, Type to); + void paintLottieIcon(QPainter &p, int index, bool over); + const style::SendButton &_st; State _state; @@ -87,6 +99,9 @@ private: QString _slowmodeDelayText; Ui::Text::String _starsToSendText; + std::array, 2> _voiceRoundIcons; + bool _voiceRoundAnimating = false; + }; struct SendStarButtonState { diff --git a/Telegram/SourceFiles/window/themes/window_theme_preview.cpp b/Telegram/SourceFiles/window/themes/window_theme_preview.cpp index 9b05d2fafd..0130e318a0 100644 --- a/Telegram/SourceFiles/window/themes/window_theme_preview.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme_preview.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_three_state_icon.h" #include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" #include "platform/platform_window_title.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" @@ -565,7 +566,13 @@ void Generator::paintComposeArea() { : st::historyAttach.iconPosition.y(); st::historyAttach.icon[_palette].paint(*_p, _composeArea.x() + attachIconLeft, controlsTop + attachIconTop, _rect.width()); auto right = st::historySendRight + st::historySendSize.width(); - st::historyRecordVoice[_palette].paintInCenter(*_p, QRect(_composeArea.x() + _composeArea.width() - right, controlsTop, st::historySendSize.width(), st::historySendSize.height())); + + const auto recordIcon = Lottie::MakeIcon({ + .path = u":/animations/chat/voice_to_video.tgs"_q, + .sizeOverride = st::historySend.recordSize, + .colorizeUsingAlpha = true, + }); + recordIcon->paintInCenter(*_p, QRect(_composeArea.x() + _composeArea.width() - right, controlsTop, st::historySendSize.width(), st::historySendSize.height()), st::historyRecordVoiceFg[_palette]->c); const auto &emojiButton = st::historyAttachEmoji.inner; const auto emojiIconLeft = (emojiButton.iconPosition.x() < 0) diff --git a/Telegram/lib_lottie b/Telegram/lib_lottie index 99d8cc4d18..cba5d113eb 160000 --- a/Telegram/lib_lottie +++ b/Telegram/lib_lottie @@ -1 +1 @@ -Subproject commit 99d8cc4d1820960fbcdafc574f1894073b14b75a +Subproject commit cba5d113eb941c3eec20c4d171ff4ff9d1a92382