diff --git a/Telegram/Resources/icons/send_control_record_active.png b/Telegram/Resources/icons/send_control_record_active.png new file mode 100644 index 0000000000..af937a2014 Binary files /dev/null and b/Telegram/Resources/icons/send_control_record_active.png differ diff --git a/Telegram/Resources/icons/send_control_record_active@2x.png b/Telegram/Resources/icons/send_control_record_active@2x.png new file mode 100644 index 0000000000..f854549ac5 Binary files /dev/null and b/Telegram/Resources/icons/send_control_record_active@2x.png differ diff --git a/Telegram/Resources/icons/send_control_record_active@3x.png b/Telegram/Resources/icons/send_control_record_active@3x.png new file mode 100644 index 0000000000..1406aadc58 Binary files /dev/null and b/Telegram/Resources/icons/send_control_record_active@3x.png differ diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp index 7240cd0e1e..0cc7349556 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp @@ -59,6 +59,43 @@ enum class FilterType { } // namespace +class RecordLevel final : public Ui::AbstractButton { +public: + RecordLevel( + not_null parent, + rpl::producer<> leaveWindowEventProducer); + + void requestPaintColor(float64 progress); + void requestPaintProgress(float64 progress); + void requestPaintLevel(quint16 level); + void reset(); + + [[nodiscard]] rpl::producer actives() const; + + [[nodiscard]] bool inCircle(const QPoint &localPos) const; + +private: + void init(); + + void drawProgress(Painter &p); + + const int _height; + const int _center; + + rpl::variable _showProgress = 0.; + rpl::variable _colorProgress = 0.; + rpl::variable _inCircle = false; + + bool recordingAnimationCallback(crl::time now); + + // This can animate for a very long time (like in music playing), + // so it should be a Basic, not a Simple animation. + Ui::Animations::Basic _recordingAnimation; + anim::value _recordingLevel; + + rpl::lifetime _showingLifetime; +}; + class RecordLock final : public Ui::RpWidget { public: RecordLock(not_null parent); @@ -79,6 +116,149 @@ private: rpl::variable _progress = 0.; }; +RecordLevel::RecordLevel( + not_null parent, + rpl::producer<> leaveWindowEventProducer) +: AbstractButton(parent) +, _height(st::historyRecordLevelMaxRadius * 2) +, _center(_height / 2) +, _recordingAnimation([=](crl::time now) { + return recordingAnimationCallback(now); +}) { + resize(_height, _height); + std::move( + leaveWindowEventProducer + ) | rpl::start_with_next([=] { + _inCircle = false; + }, lifetime()); + init(); +} + +void RecordLevel::requestPaintLevel(quint16 level) { + _recordingLevel.start(level); + _recordingAnimation.start(); +} + +bool RecordLevel::recordingAnimationCallback(crl::time now) { + const auto dt = anim::Disabled() + ? 1. + : ((now - _recordingAnimation.started()) + / float64(kRecordingUpdateDelta)); + if (dt >= 1.) { + _recordingLevel.finish(); + } else { + _recordingLevel.update(dt, anim::sineInOut); + } + if (!anim::Disabled()) { + update(); + } + return (dt < 1.); +} + +void RecordLevel::init() { + shownValue( + ) | rpl::start_with_next([=](bool shown) { + }, lifetime()); + + paintRequest( + ) | rpl::start_with_next([=](const QRect &clip) { + Painter p(this); + + drawProgress(p); + + st::historyRecordVoiceActive.paintInCenter(p, rect()); + }, lifetime()); + + _showProgress.changes( + ) | rpl::map([](auto value) { + return value != 0.; + }) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](bool show) { + setVisible(show); + setMouseTracking(show); + if (!show) { + _recordingLevel = anim::value(); + _recordingAnimation.stop(); + _showingLifetime.destroy(); + } + }, lifetime()); + + actives( + ) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](bool active) { + setPointerCursor(active); + }, lifetime()); +} + +rpl::producer RecordLevel::actives() const { + return events( + ) | rpl::filter([=](not_null e) { + return (e->type() == QEvent::MouseMove + || e->type() == QEvent::Leave + || e->type() == QEvent::Enter); + }) | rpl::map([=](not_null e) { + switch(e->type()) { + case QEvent::MouseMove: + return inCircle((static_cast(e.get()))->pos()); + case QEvent::Leave: return false; + case QEvent::Enter: return inCircle(mapFromGlobal(QCursor::pos())); + default: return false; + } + }); +} + +bool RecordLevel::inCircle(const QPoint &localPos) const { + const auto &radii = st::historyRecordLevelMaxRadius; + const auto dx = std::abs(localPos.x() - _center); + if (dx > radii) { + return false; + } + const auto dy = std::abs(localPos.y() - _center); + if (dy > radii) { + return false; + } else if (dx + dy <= radii) { + return true; + } + return ((dx * dx + dy * dy) <= (radii * radii)); +} + +void RecordLevel::drawProgress(Painter &p) { + PainterHighQualityEnabler hq(p); + p.setPen(Qt::NoPen); + const auto color = anim::color( + st::historyRecordSignalColor, + st::historyRecordVoiceFgActive, + _colorProgress.current()); + p.setBrush(color); + + const auto progress = _showProgress.current(); + + const auto center = QPoint(_center, _center); + const int mainRadii = progress * st::historyRecordLevelMainRadius; + + { + p.setOpacity(.5); + const auto min = progress * st::historyRecordLevelMinRadius; + const auto max = progress * st::historyRecordLevelMaxRadius; + const auto delta = std::min(_recordingLevel.current() / 0x4000, 1.); + const auto radii = qRound(min + (delta * (max - min))); + p.drawEllipse(center, radii, radii); + p.setOpacity(1.); + } + + p.drawEllipse(center, mainRadii, mainRadii); +} + +void RecordLevel::requestPaintProgress(float64 progress) { + _showProgress = progress; + update(); +} + +void RecordLevel::requestPaintColor(float64 progress) { + _colorProgress = progress; + update(); +} + RecordLock::RecordLock(not_null parent) : RpWidget(parent) { resize( st::historyRecordLockTopShadow.width(), @@ -222,6 +402,9 @@ VoiceRecordBar::VoiceRecordBar( , _controller(controller) , _send(send) , _lock(std::make_unique(parent)) +, _level(std::make_unique( + parent, + _controller->widget()->leaveEvents())) , _cancelFont(st::historyRecordFont) , _recordingAnimation([=](crl::time now) { return recordingAnimationCallback(now); @@ -264,6 +447,11 @@ void VoiceRecordBar::updateLockGeometry() { _lock->moveToRight(right, _lock->y()); } +void VoiceRecordBar::updateLevelGeometry() { + const auto center = (_send->width() - _level->width()) / 2; + _level->moveToRight(st::historySendRight + center, y() + center); +} + void VoiceRecordBar::init() { hide(); // Keep VoiceRecordBar behind SendButton. @@ -275,6 +463,7 @@ void VoiceRecordBar::init() { }) | rpl::to_empty ) | rpl::start_with_next([=] { stackUnder(_send.get()); + _level->raise(); }, lifetime()); sizeValue( @@ -298,6 +487,7 @@ void VoiceRecordBar::init() { } updateMessageGeometry(); updateLockGeometry(); + updateLevelGeometry(); }, lifetime()); paintRequest( @@ -347,27 +537,17 @@ void VoiceRecordBar::init() { ) | rpl::start_with_next([=] { installClickOutsideFilter(); - _send->clicks( - ) | rpl::filter([=] { - return _send->type() == Ui::SendButton::Type::Record; - }) | rpl::start_with_next([=] { + _level->clicks( + ) | rpl::start_with_next([=] { stop(true); }, _recordingLifetime); - auto hover = _send->events( - ) | rpl::filter([=](not_null e) { - return e->type() == QEvent::Enter - || e->type() == QEvent::Leave; - }) | rpl::map([=](not_null e) { - return (e->type() == QEvent::Enter); - }); - _send->setLockRecord(true); _send->setForceRippled(true); rpl::single( false ) | rpl::then( - std::move(hover) + _level->actives() ) | rpl::start_with_next([=](bool enter) { _inField = enter; }, _recordingLifetime); @@ -398,7 +578,7 @@ void VoiceRecordBar::activeAnimate(bool active) { } else { auto callback = [=] { update(_messageRect); - _send->requestPaintRecord(activeAnimationRatio()); + _level->requestPaintColor(activeAnimationRatio()); }; const auto from = active ? 0. : 1.; _activeAnimation.start(std::move(callback), from, to, duration); @@ -410,6 +590,7 @@ void VoiceRecordBar::visibilityAnimate(bool show, Fn &&callback) { const auto from = show ? 0. : 1.; const auto duration = st::historyRecordVoiceShowDuration; auto animationCallback = [=, callback = std::move(callback)](auto value) { + _level->requestPaintProgress(value); update(); if ((show && value == 1.) || (!show && value == 0.)) { if (callback) { @@ -429,6 +610,7 @@ void VoiceRecordBar::setLockBottom(rpl::producer &&bottom) { bottom ) | rpl::start_with_next([=](int value) { _lock->moveToLeft(_lock->x(), value - _lock->height()); + updateLevelGeometry(); }, lifetime()); } @@ -474,8 +656,12 @@ void VoiceRecordBar::startRecording() { const auto type = e->type(); if (type == QEvent::MouseMove) { const auto mouse = static_cast(e.get()); - const auto localPos = mapFromGlobal(mouse->globalPos()); - _inField = rect().contains(localPos); + const auto globalPos = mouse->globalPos(); + const auto localPos = mapFromGlobal(globalPos); + const auto inField = rect().contains(localPos); + _inField = inField + ? inField + : _level->inCircle(_level->mapFromGlobal(globalPos)); if (_showLockAnimation.animating()) { return; @@ -504,6 +690,7 @@ bool VoiceRecordBar::recordingAnimationCallback(crl::time now) { } void VoiceRecordBar::recordUpdated(quint16 level, int samples) { + _level->requestPaintLevel(level); _recordingLevel.start(level); _recordingAnimation.start(); _recordingSamples = samples; @@ -698,7 +885,7 @@ void VoiceRecordBar::installClickOutsideFilter() { } else if (type == QEvent::ContextMenu || type == QEvent::Shortcut) { return Type::ShowBox; } else if (type == QEvent::MouseButtonPress) { - return (noBox && !_send->underMouse()) + return (noBox && !_inField.current()) ? Type::ShowBox : Type::Continue; } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h index dfe60f1112..79f08ac60f 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h @@ -22,6 +22,7 @@ class SessionController; namespace HistoryView::Controls { +class RecordLevel; class RecordLock; class VoiceRecordBar final : public Ui::RpWidget { @@ -56,6 +57,7 @@ private: void updateMessageGeometry(); void updateLockGeometry(); + void updateLevelGeometry(); void recordError(); void recordUpdated(quint16 level, int samples); @@ -86,6 +88,7 @@ private: const not_null _controller; const std::shared_ptr _send; const std::unique_ptr _lock; + const std::unique_ptr _level; rpl::event_stream _sendActionUpdates; rpl::event_stream _sendVoiceRequests; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 0ea46b9eb4..743831fb11 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -332,8 +332,8 @@ historyRecordVoiceShowDuration: 120; historyRecordVoiceDuration: 120; historyRecordVoice: icon {{ "send_control_record", historyRecordVoiceFg }}; historyRecordVoiceOver: icon {{ "send_control_record", historyRecordVoiceFgOver }}; -historyRecordVoiceActive: icon {{ "send_control_record", historyRecordVoiceFgActive }}; -historyRecordVoiceCancel: icon {{ "send_control_record", attentionButtonFg }}; +historyRecordVoiceActive: icon {{ "send_control_record_active", recordActiveIcon }}; +historyRecordVoiceCancel: icon {{ "send_control_record_active", attentionButtonFg }}; historyRecordVoiceRippleBgActive: lightButtonBgOver; historyRecordVoiceRippleBgCancel: attentionButtonBgRipple; historyRecordSignalColor: attentionButtonFg; @@ -345,6 +345,10 @@ historyRecordFont: font(13px); historyRecordDurationSkip: 12px; historyRecordDurationFg: historyComposeAreaFg; +historyRecordLevelMainRadius: 37px; +historyRecordLevelMinRadius: 38px; +historyRecordLevelMaxRadius: 60px; + historyRecordTextStyle: TextStyle(defaultTextStyle) { font: historyRecordFont; }