diff --git a/Telegram/SourceFiles/boxes/compose_ai_box.cpp b/Telegram/SourceFiles/boxes/compose_ai_box.cpp index 78f8eda1a0..406ab1af0b 100644 --- a/Telegram/SourceFiles/boxes/compose_ai_box.cpp +++ b/Telegram/SourceFiles/boxes/compose_ai_box.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/choose_language_box.h" #include "ui/controls/send_button.h" #include "ui/effects/ripple_animation.h" +#include "ui/effects/skeleton_animation.h" #include "ui/layers/generic_box.h" #include "ui/painter.h" #include "ui/text/custom_emoji_helper.h" @@ -54,6 +55,21 @@ enum class ComposeAiMode { Fix, }; +enum class CardState { + Waiting, + Loading, + Ready, + Failed, +}; + +[[nodiscard]] QColor ComposeAiColorWithAlpha( + const style::color &color, + float64 alpha) { + auto result = color->c; + result.setAlphaF(result.alphaF() * alpha); + return result; +} + [[nodiscard]] QString ToneTitle(const QString &key) { const auto value = Lang::GetNonDefaultValue( QByteArray("cloud_lng_ai_compose_style_") + key.toUtf8()); @@ -177,14 +193,6 @@ enum class ComposeAiMode { return st::aiComposeStyleTabsRadius; } -[[nodiscard]] QColor ComposeAiColorWithAlpha( - const style::color &color, - float64 alpha) { - auto result = color->c; - result.setAlphaF(result.alphaF() * alpha); - return result; -} - [[nodiscard]] QColor ComposeAiActiveBackgroundColor( const style::color &color) { return ComposeAiColorWithAlpha( @@ -328,9 +336,8 @@ public: void setResultTitle(const TextWithEntities &title); void setEmojifyVisible(bool visible); void setEmojifyChecked(bool checked); - void setLoading(bool loading); - void setResultVisible(bool visible); - void setResult(TextWithEntities text, bool copyVisible); + void setState(CardState state); + void setResultText(TextWithEntities text); protected: int resizeGetHeight(int newWidth) override; @@ -355,13 +362,11 @@ private: Fn _emojifyChanged; bool _originalExpanded = false; bool _originalVisible = true; - bool _loading = false; - bool _hasShownResult = false; - bool _resultVisible = true; - bool _copyVisible = false; bool _emojifyVisible = false; bool _dividerVisible = false; int _dividerTop = 0; + CardState _state = CardState::Waiting; + Ui::SkeletonAnimation _skeleton; std::array _diffColors; }; @@ -420,7 +425,7 @@ private: ComposeAiMode _mode = ComposeAiMode::Translate; int _styleIndex = -1; bool _emojify = false; - bool _ready = false; + CardState _state = CardState::Waiting; mtpRequestId _requestId = 0; int _requestToken = 0; TextWithEntities _result; @@ -1003,7 +1008,8 @@ ComposeAiPreviewCard::ComposeAiPreviewCard( this, tr::lng_ai_compose_emojify(tr::now), st::aiComposeEmojifyCheckbox, - std::make_unique(st::defaultCheck,false))) { + std::make_unique(st::defaultCheck,false))) +, _skeleton(_resultBody) { _originalBody->setSelectable(true); _originalBody->setMarkedText(_original, _context); _resultTitle->setClickHandlerFilter([=](const auto &...) { @@ -1036,7 +1042,8 @@ ComposeAiPreviewCard::ComposeAiPreviewCard( }, _emojify->lifetime()); setOriginalTitle(tr::lng_ai_compose_original(tr::now)); setResultTitle(tr::lng_ai_compose_result(tr::now, tr::marked)); - setLoading(true); + _resultBody->setMarkedText(_original, _context); + _copy->setVisible(false); updateOriginalToggleIcon(); } @@ -1088,29 +1095,38 @@ void ComposeAiPreviewCard::setEmojifyChecked(bool checked) { refreshGeometry(); } -void ComposeAiPreviewCard::setLoading(bool loading) { - _loading = loading; - if (loading && !_hasShownResult) { - _resultBody->setMarkedText(_original, _context); - } - _copy->setVisible(!loading && _copyVisible); - refreshGeometry(); -} - -void ComposeAiPreviewCard::setResultVisible(bool visible) { - if (_resultVisible == visible) { +void ComposeAiPreviewCard::setState(CardState state) { + if (_state == state) { return; } - _resultVisible = visible; + const auto wasLoading = (_state == CardState::Loading); + _state = state; + switch (_state) { + case CardState::Waiting: + case CardState::Failed: + _resultBody->setMarkedText(_original, _context); + _copy->setVisible(false); + if (wasLoading) { + _skeleton.stop(); + } + break; + case CardState::Loading: + _resultBody->setMarkedText(_original, _context); + _copy->setVisible(false); + _skeleton.start(); + break; + case CardState::Ready: + _copy->setVisible(true); + if (wasLoading) { + _skeleton.stop(); + } + break; + } refreshGeometry(); } -void ComposeAiPreviewCard::setResult(TextWithEntities text, bool copyVisible) { - _loading = false; - _hasShownResult = true; - _copyVisible = copyVisible; - _resultBody->setMarkedText(text, _context); - _copy->setVisible(_copyVisible); +void ComposeAiPreviewCard::setResultText(TextWithEntities text) { + _resultBody->setMarkedText(std::move(text), _context); refreshGeometry(); } @@ -1165,70 +1181,61 @@ int ComposeAiPreviewCard::resizeGetHeight(int newWidth) { _originalToggle->hide(); } - if (_resultVisible) { - _resultTitle->show(); - _resultBody->show(); - auto controlsWidth = 0; - if (_emojifyVisible) { - _emojify->show(); - _emojify->resizeToNaturalWidth(contentWidth); - controlsWidth += _emojify->width() - + st::aiComposeCardControlSkip; - } else { - _emojify->hide(); - } - const auto resultTitleWidth = std::max( - contentWidth - controlsWidth, - 0); - _resultTitle->resizeToWidth(resultTitleWidth); - auto right = padding.right(); - if (_emojifyVisible) { - _emojify->moveToRight(right, y, newWidth); - right += _emojify->width() + st::aiComposeCardControlSkip; - } - _resultTitle->setGeometryToLeft( - padding.left(), - y, - resultTitleWidth, - _resultTitle->height(), - newWidth); - y += std::max({ - _resultTitle->height(), - _emojifyVisible ? _emojify->height() : 0, - }); - y += st::aiComposeCardTextSkip; - - const auto lineHeight = _resultBody->st().style.lineHeight - ? _resultBody->st().style.lineHeight - : _resultBody->st().style.font->height; - if (_copy->isVisible()) { - _resultBody->setSkipBlock( - _copy->width(), - lineHeight); - } else { - _resultBody->setSkipBlock(0, 0); - } - _resultBody->resizeToWidth(contentWidth); - _resultBody->setGeometryToLeft( - padding.left(), - y, - contentWidth, - _resultBody->height(), - newWidth); - if (_copy->isVisible()) { - _copy->moveToRight( - padding.right(), - y + _resultBody->height() - lineHeight, - newWidth); - } - y += _resultBody->height(); - + _resultTitle->show(); + auto controlsWidth = 0; + if (_emojifyVisible) { + _emojify->show(); + _emojify->resizeToNaturalWidth(contentWidth); + controlsWidth += _emojify->width() + + st::aiComposeCardControlSkip; } else { - _resultTitle->hide(); - _resultBody->hide(); - _copy->hide(); _emojify->hide(); } + const auto resultTitleWidth = std::max( + contentWidth - controlsWidth, + 0); + _resultTitle->resizeToWidth(resultTitleWidth); + auto right = padding.right(); + if (_emojifyVisible) { + _emojify->moveToRight(right, y, newWidth); + right += _emojify->width() + st::aiComposeCardControlSkip; + } + _resultTitle->setGeometryToLeft( + padding.left(), + y, + resultTitleWidth, + _resultTitle->height(), + newWidth); + y += std::max({ + _resultTitle->height(), + _emojifyVisible ? _emojify->height() : 0, + }); + y += st::aiComposeCardTextSkip; + + const auto lineHeight = _resultBody->st().style.lineHeight + ? _resultBody->st().style.lineHeight + : _resultBody->st().style.font->height; + if (_copy->isVisible()) { + _resultBody->setSkipBlock( + _copy->width(), + lineHeight); + } else { + _resultBody->setSkipBlock(0, 0); + } + _resultBody->resizeToWidth(contentWidth); + _resultBody->setGeometryToLeft( + padding.left(), + y, + contentWidth, + _resultBody->height(), + newWidth); + if (_copy->isVisible()) { + _copy->moveToRight( + padding.right(), + y + _resultBody->height() - lineHeight, + newWidth); + } + y += _resultBody->height(); return y + padding.bottom(); } @@ -1298,7 +1305,7 @@ ComposeAiContent::~ComposeAiContent() { } bool ComposeAiContent::hasResult() const { - return _ready; + return _state == CardState::Ready; } const TextWithEntities &ComposeAiContent::result() const { @@ -1385,7 +1392,7 @@ void ComposeAiContent::chooseLanguage() { } void ComposeAiContent::copyResult() { - if (!_ready) { + if (_state != CardState::Ready) { return; } TextUtilities::SetClipboardText( @@ -1397,6 +1404,8 @@ void ComposeAiContent::setMode(ComposeAiMode mode) { return; } _mode = mode; + _state = CardState::Waiting; + _preview->setState(CardState::Waiting); if (_modeChanged) { _modeChanged(_mode); } @@ -1407,20 +1416,21 @@ void ComposeAiContent::setMode(ComposeAiMode mode) { } void ComposeAiContent::updateTitles() { - const auto styleNoSelection = (_mode == ComposeAiMode::Style) - && (_styleIndex < 0); - _preview->setOriginalVisible( - _mode != ComposeAiMode::Style || styleNoSelection); - _preview->setResultVisible(!styleNoSelection); + const auto hasResult = (_state == CardState::Loading) + || (_state == CardState::Ready); + _preview->setOriginalVisible(hasResult); _preview->setOriginalTitle( (_mode == ComposeAiMode::Translate) ? FromTitle(_detectedFrom) : tr::lng_ai_compose_original(tr::now)); _preview->setResultTitle( - (_mode == ComposeAiMode::Translate) - ? ToTitle(_to) - : tr::lng_ai_compose_result(tr::now, tr::marked)); - _preview->setEmojifyVisible(_mode != ComposeAiMode::Fix); + hasResult + ? ((_mode == ComposeAiMode::Translate) + ? ToTitle(_to) + : tr::lng_ai_compose_result(tr::now, tr::marked)) + : tr::lng_ai_compose_original(tr::now, tr::marked)); + _preview->setEmojifyVisible( + hasResult && (_mode != ComposeAiMode::Fix)); _preview->setEmojifyChecked(_emojify); } @@ -1449,9 +1459,10 @@ void ComposeAiContent::request() { return; } cancelRequest(); - _ready = false; + _state = CardState::Loading; _result = {}; - _preview->setLoading(true); + _preview->setState(CardState::Loading); + updateTitles(); notifyReadyChanged(); auto request = Api::ComposeWithAi::Request{ @@ -1495,17 +1506,20 @@ void ComposeAiContent::applyResult(Api::ComposeWithAi::Result &&result) { auto display = (_mode == ComposeAiMode::Fix && result.diffText) ? BuildDiffDisplay(*result.diffText) : _result; - _preview->setResult(std::move(display), true); - _ready = !_result.text.isEmpty(); + _state = _result.text.isEmpty() ? CardState::Failed : CardState::Ready; + _preview->setState(_state); + if (_state == CardState::Ready) { + _preview->setResultText(std::move(display)); + } + updateTitles(); notifyReadyChanged(); refreshLayout(); } void ComposeAiContent::showError(const MTP::Error &error) { - _preview->setResult( - Ui::Text::Italic(tr::lng_ai_compose_error(tr::now)), - false); - _ready = false; + _state = CardState::Failed; + _preview->setState(CardState::Failed); + updateTitles(); notifyReadyChanged(); refreshLayout(); if (error.type() == u"AICOMPOSE_FLOOD_PREMIUM"_q) { @@ -1533,7 +1547,7 @@ void ComposeAiContent::showError(const MTP::Error &error) { void ComposeAiContent::notifyReadyChanged() { if (_readyChanged) { - _readyChanged(_ready); + _readyChanged(_state == CardState::Ready); } } @@ -1652,7 +1666,7 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { btn->setDisabled(true); btn->setTextFgOverride( anim::color(st::activeButtonBg, st::activeButtonFg, 0.5)); - } else { + } else if (content->hasResult()) { box->setStyle(st::aiComposeBoxWithSend); const auto isStyle = (content->mode() == ComposeAiMode::Style); @@ -1662,7 +1676,6 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { : tr::lng_ai_compose_apply(), applyAndClose); btn->setFullRadius(true); - btn->setDisabled(!content->hasResult()); *applyButton = btn; const auto send = Ui::CreateChild( @@ -1670,9 +1683,6 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { st::aiComposeSendButton); send->setState({ .type = Ui::SendButton::Type::Send }); send->show(); - send->setAttribute( - Qt::WA_TransparentForMouseEvents, - !content->hasResult()); btn->geometryValue( ) | rpl::on_next([=](QRect geometry) { const auto size = st::aiComposeBoxWithSend.buttonHeight; @@ -1684,17 +1694,23 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { }, send->lifetime()); send->setClickedCallback(applyAndClose); *sendButton = send; + } else { + box->setStyle(st::aiComposeBox); + const auto isStyle = + (content->mode() == ComposeAiMode::Style); + const auto btn = box->addButton( + isStyle + ? tr::lng_ai_compose_apply_style() + : tr::lng_ai_compose_apply(), + nullptr); + btn->setFullRadius(true); + btn->setDisabled(true); + *applyButton = btn; } }; - content->setReadyChangedCallback([=](bool ready) { - if (*applyButton) { - (*applyButton)->setDisabled(!ready); - } - if (*sendButton) { - (*sendButton)->setAttribute( - Qt::WA_TransparentForMouseEvents, !ready); - } + content->setReadyChangedCallback([=](bool) { + rebuildButtons(); }); content->setPremiumFloodCallback([=] { *premiumFlooded = true; diff --git a/Telegram/SourceFiles/ui/effects/skeleton_animation.cpp b/Telegram/SourceFiles/ui/effects/skeleton_animation.cpp new file mode 100644 index 0000000000..7279c97f89 --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/skeleton_animation.cpp @@ -0,0 +1,138 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "ui/effects/skeleton_animation.h" + +#include "ui/painter.h" +#include "ui/widgets/labels.h" +#include "styles/style_widgets.h" + +namespace Ui { +namespace { + +constexpr auto kSlideDuration = crl::time(1000); +constexpr auto kWaitDuration = crl::time(1000); +constexpr auto kFullDuration = kSlideDuration + kWaitDuration; +constexpr auto kBaseAlpha = 0.5; +constexpr auto kGradientAlpha = 0.2; + +[[nodiscard]] QColor ColorWithAlpha( + const style::color &color, + float64 alpha) { + auto result = color->c; + result.setAlphaF(result.alphaF() * alpha); + return result; +} + +} // namespace + +SkeletonAnimation::SkeletonAnimation(not_null label) +: _label(label) +, _animation([=] { _label->update(); }) { + base::install_event_filter(_label, _label, [=](not_null e) { + return (_active && e->type() == QEvent::Paint) + ? paint() + : base::EventFilterResult::Continue; + }); + _label->sizeValue( + ) | rpl::on_next([=](QSize) { + if (_active) { + recompute(); + } + }, _lifetime); +} + +void SkeletonAnimation::start() { + _active = true; + recompute(); + _animation.start(); + _label->update(); +} + +void SkeletonAnimation::stop() { + _active = false; + _animation.stop(); + _label->update(); +} + +bool SkeletonAnimation::active() const { + return _active; +} + +void SkeletonAnimation::recompute() { + _lineWidths = _label->countLineWidths(); +} + +base::EventFilterResult SkeletonAnimation::paint() { + if (_lineWidths.empty()) { + return base::EventFilterResult::Cancel; + } + auto p = QPainter(_label); + PainterHighQualityEnabler hq(p); + + const auto &st = _label->st(); + const auto lineHeight = st.style.lineHeight + ? st.style.lineHeight + : st.style.font->height; + const auto thickness = st.style.font->height / 2; + const auto radius = thickness / 2.; + const auto left = st.margin.left(); + const auto textWidth = _label->width() + - st.margin.left() + - st.margin.right(); + const auto baseColor = ColorWithAlpha( + st::windowSubTextFg, + kBaseAlpha); + + const auto now = crl::now(); + const auto period = now % kFullDuration; + const auto gradientActive = (period < kSlideDuration); + + p.setPen(Qt::NoPen); + if (gradientActive) { + const auto progress = period + / float64(kSlideDuration); + const auto gradientWidth = textWidth; + const auto gradientStart = anim::interpolate( + left - gradientWidth, + left + textWidth, + progress); + auto gradient = QLinearGradient( + gradientStart, + 0, + gradientStart + gradientWidth, + 0); + const auto centerColor = ColorWithAlpha( + st::windowSubTextFg, + kGradientAlpha); + gradient.setStops({ + { 0., baseColor }, + { 0.5, centerColor }, + { 1., baseColor }, + }); + p.setBrush(gradient); + } else { + p.setBrush(baseColor); + } + auto lineTop = st.margin.top(); + for (const auto lineWidth : _lineWidths) { + if (lineWidth <= 0) { + lineTop += lineHeight; + continue; + } + const auto rect = QRectF( + left, + lineTop + (lineHeight - thickness) / 2., + lineWidth, + thickness); + p.drawRoundedRect(rect, radius, radius); + lineTop += lineHeight; + } + return base::EventFilterResult::Cancel; +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/effects/skeleton_animation.h b/Telegram/SourceFiles/ui/effects/skeleton_animation.h new file mode 100644 index 0000000000..c8f6ab210b --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/skeleton_animation.h @@ -0,0 +1,37 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/event_filter.h" +#include "ui/effects/animations.h" + +namespace Ui { + +class FlatLabel; + +class SkeletonAnimation final { +public: + explicit SkeletonAnimation(not_null label); + + void start(); + void stop(); + [[nodiscard]] bool active() const; + +private: + void recompute(); + base::EventFilterResult paint(); + + const not_null _label; + Ui::Animations::Basic _animation; + std::vector _lineWidths; + bool _active = false; + rpl::lifetime _lifetime; + +}; + +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 059dc541bb..5bcf551fd0 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -481,6 +481,8 @@ PRIVATE ui/effects/round_checkbox.h ui/effects/scroll_content_shadow.cpp ui/effects/scroll_content_shadow.h + ui/effects/skeleton_animation.cpp + ui/effects/skeleton_animation.h ui/effects/shake_animation.cpp ui/effects/shake_animation.h ui/effects/snowflakes.cpp diff --git a/Telegram/lib_ui b/Telegram/lib_ui index fe9ce25b40..b308637783 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit fe9ce25b409db84eeb8adeb827bf5cd18ade303c +Subproject commit b308637783045577d11b4b2c071ddb8e9be29fb2