diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index e686508b3..2decd8466 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -479,6 +479,8 @@ PRIVATE history/view/controls/history_view_compose_controls.h history/view/controls/history_view_voice_record_bar.cpp history/view/controls/history_view_voice_record_bar.h + history/view/controls/history_view_voice_record_button.cpp + history/view/controls/history_view_voice_record_button.h history/view/media/history_view_call.h history/view/media/history_view_call.cpp history/view/media/history_view_contact.h 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 48e328ea0..1a85e370b 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 @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/event_filter.h" #include "boxes/confirm_box.h" #include "core/application.h" +#include "history/view/controls/history_view_voice_record_button.h" #include "lang/lang_keys.h" #include "mainwindow.h" #include "media/audio/media_audio.h" @@ -59,42 +60,6 @@ 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); - - [[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); @@ -117,148 +82,6 @@ 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() { - const auto hasProgress = [](auto value) { return value != 0.; }; - - paintRequest( - ) | rpl::start_with_next([=](const QRect &clip) { - Painter p(this); - - drawProgress(p); - - p.setOpacity(_showProgress.current()); - st::historyRecordVoiceActive.paintInCenter(p, rect()); - }, lifetime()); - - rpl::merge( - shownValue(), - _showProgress.value() | rpl::map(hasProgress) - ) | rpl::start_with_next([=](bool show) { - setVisible(show); - setMouseTracking(show); - if (!show) { - _recordingLevel = anim::value(); - _recordingAnimation.stop(); - _showingLifetime.destroy(); - _showProgress = 0.; - } - }, 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(), @@ -419,7 +242,7 @@ VoiceRecordBar::VoiceRecordBar( , _controller(controller) , _send(send) , _lock(std::make_unique(sectionWidget)) -, _level(std::make_unique( +, _level(std::make_unique( sectionWidget, _controller->widget()->leaveEvents())) , _startTimer([=] { startRecording(); }) 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 a70a0e4db..16b7447d3 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 @@ -23,7 +23,7 @@ class SessionController; namespace HistoryView::Controls { -class RecordLevel; +class VoiceRecordButton; class RecordLock; class VoiceRecordBar final : public Ui::RpWidget { @@ -100,7 +100,7 @@ private: const not_null _controller; const std::shared_ptr _send; const std::unique_ptr _lock; - const std::unique_ptr _level; + const std::unique_ptr _level; base::Timer _startTimer; 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 new file mode 100644 index 000000000..bb9b81c0e --- /dev/null +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp @@ -0,0 +1,746 @@ +/* +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 "history/view/controls/history_view_voice_record_button.h" + +#include "styles/style_chat.h" +#include "styles/style_layers.h" + +#include + +namespace HistoryView::Controls { + +namespace { + +constexpr auto kRecordingUpdateDelta = crl::time(100); + +constexpr auto kSegmentsCount = 12; +constexpr auto kMajorDegreeOffset = 360 / kSegmentsCount; +constexpr auto kSixtyDegrees = 60; + +constexpr auto kEnterIdleAnimationDuration = crl::time(1200); + +constexpr auto kRotationSpeed = 0.36 * 0.1; + +constexpr auto kRandomAdditionFactor = 0.3; + +constexpr auto kIdleRadiusGlobalFactor = 0.56; +constexpr auto kIdleRadiusFactor = 0.15 * 0.5; + +constexpr auto kOpacityMajor = 0.30; +constexpr auto kOpacityMinor = 0.15; + +constexpr auto kIdleRotationSpeed = 0.2; +constexpr auto kIdleRotateDiff = 0.1 * kIdleRotationSpeed; + +constexpr auto kWaveAngle = 0.03; + +constexpr auto kAnimationSpeedMajor = 1.5;// - 0.65; +constexpr auto kAnimationSpeedMinor = 1.5;// - 0.45; +constexpr auto kAnimationSpeedCircle = 1.5;// - 0.25; + +constexpr auto kAmplitudeDiffFactorMax = 500.; +constexpr auto kAmplitudeDiffFactorMajor = 300.; +constexpr auto kAmplitudeDiffFactorMinor = 400.; + +constexpr auto kFlingDistanceFactorMajor = 8 * 16; +constexpr auto kFlingDistanceFactorMinor = 20 * 16; + +constexpr auto kFlingInAnimationDurationMajor = 200; +constexpr auto kFlingInAnimationDurationMinor = 350; +constexpr auto kFlingOutAnimationDurationMajor = 220; +constexpr auto kFlingOutAnimationDurationMinor = 380; + +constexpr auto kSineWaveSpeedMajor = 0.02 * 0.2 * 0.5; +constexpr auto kSineWaveSpeedMinor = 0.026 * 0.2 * 0.5; + +constexpr auto kSmallWaveRadius = 0.55; + +constexpr auto kFlingDistance = 0.50; + +constexpr auto kMinDivider = 100.; + +constexpr auto kMaxAmplitude = 1800.; + +constexpr auto kZeroPoint = QPointF(0, 0); + +void ApplyTo(float64 &value, const float64 &to, const float64 &diff) { + if ((value != to) && ((diff > 0) == (value > to))) { + value = to; + } +} + +template +void Normalize(Number &value, Number right) { + if (value >= right) { + value -= right; + } +} + +float64 RandomAdditional() { + return (rand_value() % 100 / 100.); +} + +void PerformAnimation( + rpl::producer &&animationTicked, + Fn &&applyValue, + Fn &&finishCallback, + float64 duration, + float64 from, + float64 to, + rpl::lifetime &lifetime) { + lifetime.destroy(); + const auto animValue = + lifetime.make_state(from, to); + const auto animStarted = crl::now(); + std::move( + animationTicked + ) | rpl::start_with_next([=, + applyValue = std::move(applyValue), + finishCallback = std::move(finishCallback), + &lifetime](crl::time now) mutable { + const auto dt = anim::Disabled() + ? 1. + : ((now - animStarted) / duration); + if (dt >= 1.) { + animValue->finish(); + applyValue(animValue->current()); + lifetime.destroy(); + if (finishCallback) { + finishCallback(); + } + } else { + animValue->update(dt, anim::linear); + applyValue(animValue->current()); + } + }, lifetime); +} + +} // namespace + +class CircleBezier final { +public: + CircleBezier(int n); + + void computeRandomAdditionals(); + void paintCircle( + Painter &p, + const QColor &c, + float64 radius, + float64 cubicBezierFactor, + float64 idleStateDiff, + float64 radiusDiff, + float64 randomFactor); + +private: + struct Points { + QPointF point; + QPointF control; + }; + + const int _segmentsCount; + const float64 _segmentLength; + std::vector _randomAdditionals; + +}; + +class Wave final { +public: + Wave( + rpl::producer animationTicked, + int n, + float64 rotationOffset, + float64 amplitudeRadius, + float64 amplitudeWaveDiff, + float64 fling, + int flingDistanceFactor, + int flingInAnimationDuration, + int flingOutAnimationDuration, + float64 amplitudeDiffSpeed, + float64 amplitudeDiffFactor, + bool isDirectionClockwise); + + void setValue(float64 value); + void tick(float64 circleRadius, crl::time lastUpdateTime); + + void paint(Painter &p, QColor c); + +private: + + void initEnterIdleAnimation(rpl::producer animationTicked); + void initFlingAnimation(rpl::producer animationTicked); + + Ui::Animations::Simple _flingAnimation; + + const std::unique_ptr _circleBezier; + + const float _rotationOffset; + const float64 _idleGlobalRadius; + const float64 _amplitudeRadius; + const float64 _amplitudeWaveDiff; + const float64 _randomAdditions; + const float64 _fling; + const int _flingDistanceFactor; + const int _flingInAnimationDuration; + const int _flingOutAnimationDuration; + const float64 _amplitudeDiffSpeed; + const float64 _amplitudeDiffFactor; + const int _directionClockwise; + + bool _incRandomAdditionals = false; + bool _isIdle = true; + bool _wasFling = false; + float64 _amplitude = 0.; + float64 _animateAmplitudeDiff = 0.; + float64 _animateAmplitudeSlowDiff = 0.; + float64 _animateToAmplitude = 0.; + float64 _flingRadius = 0.; + float64 _idleRadius = 0.; + float64 _idleRotation = 0.; + float64 _lastRadius = 0.; + float64 _rotation = 0.; + float64 _sineAngleMax = 0.; + float64 _slowAmplitude = 0.; + float64 _waveAngle = 0.; + float64 _waveDiff = 0.; + + rpl::event_stream _flingAnimationRequests; + rpl::event_stream<> _enterIdleAnimationRequests; + rpl::lifetime _animationEnterIdleLifetime; + rpl::lifetime _animationFlingLifetime; + rpl::lifetime _lifetime; +}; + +class RecordCircle final { +public: + RecordCircle(rpl::producer animationTicked); + + void setAmplitude(float64 value); + void paint(Painter &p, QColor c); + +private: + + const std::unique_ptr _majorWave; + const std::unique_ptr _minorWave; + + float64 _amplitude = 0.; + float64 _animateToAmplitude = 0.; + float64 _animateAmplitudeDiff = 0.; + crl::time _lastUpdateTime = 0; + + rpl::lifetime _animationLifetime; + rpl::lifetime _lifetime; + +}; + +CircleBezier::CircleBezier(int n) +: _segmentsCount(n) +, _segmentLength((4.0 / 3.0) * std::tan(M_PI / (2 * n))) +, _randomAdditionals(n) { +} + +void CircleBezier::computeRandomAdditionals() { + ranges::generate(_randomAdditionals, RandomAdditional); +} + +void CircleBezier::paintCircle( + Painter &p, + const QColor &c, + float64 radius, + float64 cubicBezierFactor, + float64 idleStateDiff, + float64 radiusDiff, + float64 randomFactor) { + PainterHighQualityEnabler hq(p); + + const auto r1 = radius - idleStateDiff / 2. - radiusDiff / 2.; + const auto r2 = radius + radiusDiff / 2. + idleStateDiff / 2.; + const auto l = _segmentLength * std::max(r1, r2) * cubicBezierFactor; + + auto m = QMatrix(); + + const auto preparePoints = [&](int i, bool isStart) -> Points { + Normalize(i, _segmentsCount); + const auto randomAddition = randomFactor * _randomAdditionals[i]; + const auto r = ((i % 2 == 0) ? r1 : r2) + randomAddition; + + m.reset(); + m.rotate(360. / _segmentsCount * i); + const auto sign = isStart ? 1 : -1; + + return { + (isStart && i) ? QPointF() : m.map(QPointF(0, -r)), + m.map(QPointF(sign * (l + randomAddition * _segmentLength), -r)), + }; + }; + + const auto &[startPoint, _] = preparePoints(0, true); + + auto path = QPainterPath(); + path.moveTo(startPoint); + + for (auto i = 0; i < _segmentsCount; i++) { + const auto &[_, startControl] = preparePoints(i, true); + const auto &[end, endControl] = preparePoints(i + 1, false); + + path.cubicTo(startControl, endControl, end); + } + + p.setBrush(Qt::NoBrush); + + auto pen = QPen(Qt::NoPen); + pen.setCapStyle(Qt::RoundCap); + pen.setJoinStyle(Qt::RoundJoin); + + p.setPen(pen); + p.fillPath(path, c); + p.drawPath(path); +} + +Wave::Wave( + rpl::producer animationTicked, + int n, + float64 rotationOffset, + float64 amplitudeRadius, + float64 amplitudeWaveDiff, + float64 fling, + int flingDistanceFactor, + int flingInAnimationDuration, + int flingOutAnimationDuration, + float64 amplitudeDiffSpeed, + float64 amplitudeDiffFactor, + bool isDirectionClockwise) +: _circleBezier(std::make_unique(n)) +, _rotationOffset(rotationOffset) +, _idleGlobalRadius(st::historyRecordRadiusDiffMin * kIdleRadiusGlobalFactor) +, _amplitudeRadius(amplitudeRadius) +, _amplitudeWaveDiff(amplitudeWaveDiff) +, _randomAdditions(st::historyRecordRandomAddition * kRandomAdditionFactor) +, _fling(fling) +, _flingDistanceFactor(flingDistanceFactor) +, _flingInAnimationDuration(flingInAnimationDuration) +, _flingOutAnimationDuration(flingOutAnimationDuration) +, _amplitudeDiffSpeed(amplitudeDiffSpeed) +, _amplitudeDiffFactor(amplitudeDiffFactor) +, _directionClockwise(isDirectionClockwise ? 1 : -1) +, _rotation(rotationOffset) { + initEnterIdleAnimation(rpl::duplicate(animationTicked)); + initFlingAnimation(std::move(animationTicked)); +} + +void Wave::setValue(float64 value) { + _animateToAmplitude = value; + + const auto amplitudeDelta = (_animateToAmplitude - _amplitude); + const auto amplitudeSlowDelta = (_animateToAmplitude - _slowAmplitude); + const auto factor = (_animateToAmplitude <= _amplitude) + ? kAmplitudeDiffFactorMax + : _amplitudeDiffFactor; + _animateAmplitudeDiff = amplitudeDelta + / (kMinDivider + factor * _amplitudeDiffSpeed); + _animateAmplitudeSlowDiff = amplitudeSlowDelta + / (kMinDivider + kAmplitudeDiffFactorMax * _amplitudeDiffSpeed); + + const auto idle = value < 0.1; + if (_isIdle != idle && idle) { + _enterIdleAnimationRequests.fire({}); + } + + _isIdle = idle; + + if (!_isIdle) { + _animationEnterIdleLifetime.destroy(); + } +} + +void Wave::initEnterIdleAnimation(rpl::producer animationTicked) { + _enterIdleAnimationRequests.events( + ) | rpl::start_with_next([=] { + const auto &k = kSixtyDegrees; + + const auto rotation = _rotation; + const auto rotationTo = std::round(rotation / k) * k + + _rotationOffset; + const auto waveDiff = _waveDiff; + + auto applyValue = [=](float64 v) { + _rotation = rotationTo + (rotation - rotationTo) * v; + _waveDiff = 1. + (waveDiff - 1.) * v; + _waveAngle = std::acos(_waveDiff * _directionClockwise); + }; + + PerformAnimation( + rpl::duplicate(animationTicked), + std::move(applyValue), + nullptr, + kEnterIdleAnimationDuration, + 1, + 0, + _animationEnterIdleLifetime); + + }, _lifetime); +} + +void Wave::initFlingAnimation(rpl::producer animationTicked) { + _flingAnimationRequests.events( + ) | rpl::start_with_next([=](float64 delta) { + + const auto fling = _fling * 2; + const auto flingDistance = delta + * _amplitudeRadius + * _flingDistanceFactor + * fling; + + const auto applyValue = [=](float64 v) { + _flingRadius = v; + }; + auto finishCallback = [=] { + PerformAnimation( + rpl::duplicate(animationTicked), + applyValue, + nullptr, + _flingOutAnimationDuration * fling, + flingDistance, + 0, + _animationFlingLifetime); + }; + + PerformAnimation( + rpl::duplicate(animationTicked), + applyValue, + std::move(finishCallback), + _flingInAnimationDuration * fling, + _flingRadius, + flingDistance, + _animationFlingLifetime); + + }, _lifetime); +} + +void Wave::tick(float64 circleRadius, crl::time lastUpdateTime) { + const auto dt = (crl::now() - lastUpdateTime); + + if (_animateToAmplitude != _amplitude) { + _amplitude += _animateAmplitudeDiff * dt; + ApplyTo(_amplitude, _animateToAmplitude, _animateAmplitudeDiff); + + if (std::abs(_amplitude - _animateToAmplitude) * _amplitudeRadius + < (st::historyRecordRandomAddition / 2)) { + if (!_wasFling) { + _flingAnimationRequests.fire_copy(_animateAmplitudeDiff); + _wasFling = true; + } + } else { + _wasFling = false; + } + } + + if (_animateToAmplitude != _slowAmplitude) { + _slowAmplitude += _animateAmplitudeSlowDiff * dt; + if (std::abs(_slowAmplitude - _amplitude) > 0.2) { + _slowAmplitude = _amplitude + (_slowAmplitude > _amplitude ? + 0.2 : -0.2); + } + ApplyTo(_slowAmplitude, + _animateToAmplitude, + _animateAmplitudeSlowDiff); + } + + _idleRadius = circleRadius * kIdleRadiusFactor; + + { + const auto delta = (_sineAngleMax - _animateToAmplitude); + if (std::abs(delta) - 0.25 < 0) { + _sineAngleMax = _animateToAmplitude; + } else { + _sineAngleMax -= 0.25 * ((delta < 0) ? -1 : 1); + } + } + + if (!_isIdle) { + _rotation += dt + * (kRotationSpeed * 4. * (_amplitude > 0.5 ? 1 : _amplitude / 0.5) + + kRotationSpeed * 0.5); + Normalize(_rotation, 360.); + } else { + _idleRotation += kIdleRotateDiff * dt; + Normalize(_idleRotation, 360.); + } + + _lastRadius = circleRadius; + + if (!_isIdle) { + _waveAngle += (_amplitudeWaveDiff * _sineAngleMax) * dt; + _waveDiff = std::cos(_waveAngle) * _directionClockwise; + + if ((_waveDiff != 0) && ((_waveDiff > 0) == _incRandomAdditionals)) { + _circleBezier->computeRandomAdditionals(); + _incRandomAdditionals = !_incRandomAdditionals; + } + } +} + + +void Wave::paint(Painter &p, QColor c) { + const auto waveAmplitude = _amplitude < 0.3 ? _amplitude / 0.3 : 1.; + const auto radiusDiff = st::historyRecordRadiusDiffMin + + st::historyRecordRadiusDiff * kWaveAngle * _animateToAmplitude; + + const auto diffFactor = 0.35 * waveAmplitude * _waveDiff; + + const auto radius = (_lastRadius + _amplitudeRadius * _amplitude) + + _idleGlobalRadius + + (_flingRadius * waveAmplitude); + + const auto cubicBezierFactor = 1. + + std::abs(diffFactor) * waveAmplitude + + (1. - waveAmplitude) * kIdleRadiusFactor; + + const auto circleRadiusDiff = std::max( + radiusDiff * diffFactor, + st::historyRecordLevelMainRadius - radius); + + p.rotate((_rotation + _idleRotation) * _directionClockwise); + + _circleBezier->paintCircle( + p, + c, + radius, + cubicBezierFactor, + _idleRadius * (1. - waveAmplitude), + circleRadiusDiff, + waveAmplitude * _waveDiff * _randomAdditions); + + p.rotate(0); +} + +RecordCircle::RecordCircle(rpl::producer animationTicked) +: _majorWave(std::make_unique( + rpl::duplicate(animationTicked), + kSegmentsCount, + kMajorDegreeOffset, + st::historyRecordMajorAmplitudeRadius, + kSineWaveSpeedMajor, + 0., + kFlingDistanceFactorMajor, + kFlingInAnimationDurationMajor, + kFlingOutAnimationDurationMajor, + kAnimationSpeedMajor, + kAmplitudeDiffFactorMajor, + true)) +, _minorWave(std::make_unique( + std::move(animationTicked), + kSegmentsCount, + 0, + st::historyRecordMinorAmplitudeRadius + + st::historyRecordMinorAmplitudeRadius * kSmallWaveRadius, + kSineWaveSpeedMinor, + kFlingDistance, + kFlingDistanceFactorMinor, + kFlingInAnimationDurationMinor, + kFlingOutAnimationDurationMinor, + kAnimationSpeedMinor, + kAmplitudeDiffFactorMinor, + false)) { +} + +void RecordCircle::setAmplitude(float64 value) { + _animateToAmplitude = std::min(kMaxAmplitude, value) / kMaxAmplitude; + _majorWave->setValue(_animateToAmplitude); + _minorWave->setValue(_animateToAmplitude); + _animateAmplitudeDiff = (_animateToAmplitude - _amplitude) + / (kMinDivider + kAmplitudeDiffFactorMax * kAnimationSpeedCircle); +} + +void RecordCircle::paint(Painter &p, QColor c) { + + const auto dt = (crl::now() - _lastUpdateTime); + if (_animateToAmplitude != _amplitude) { + _amplitude += _animateAmplitudeDiff * dt; + ApplyTo(_amplitude, _animateToAmplitude, _animateAmplitudeDiff); + } + + const auto radius = (st::historyRecordLevelMainRadius + + st::historyRecordLevelMainRadiusAmplitude * _amplitude); + + _majorWave->tick(radius, _lastUpdateTime); + _minorWave->tick(radius, _lastUpdateTime); + _lastUpdateTime = crl::now(); + + const auto opacity = p.opacity(); + p.setOpacity(kOpacityMajor); + _majorWave->paint(p, c); + p.setOpacity(kOpacityMinor); + _minorWave->paint(p, c); + p.setOpacity(opacity); + + p.setPen(Qt::NoPen); + p.setBrush(c); + p.drawEllipse(kZeroPoint, radius, radius); +} + +VoiceRecordButton::VoiceRecordButton( + not_null parent, + rpl::producer<> leaveWindowEventProducer) +: AbstractButton(parent) +, _recordCircle(std::make_unique( + _recordAnimationTicked.events())) +, _height(st::historyRecordLevelMaxRadius * 2) +, _center(_height / 2) +, _recordingAnimation([=](crl::time now) { + update(); + _recordAnimationTicked.fire_copy(now); + return true; +}) { + resize(_height, _height); + std::move( + leaveWindowEventProducer + ) | rpl::start_with_next([=] { + _inCircle = false; + }, lifetime()); + init(); +} + +VoiceRecordButton::~VoiceRecordButton() = default; + +void VoiceRecordButton::requestPaintLevel(quint16 level) { + _recordCircle->setAmplitude(level); + update(); +} + +bool VoiceRecordButton::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 VoiceRecordButton::init() { + const auto hasProgress = [](auto value) { return value != 0.; }; + + paintRequest( + ) | rpl::start_with_next([=](const QRect &clip) { + Painter p(this); + + p.translate(_center, _center); + PainterHighQualityEnabler hq(p); + const auto color = anim::color( + st::historyRecordSignalColor, + st::historyRecordVoiceFgActive, + _colorProgress.current()); + _recordCircle->paint(p, color); + p.resetTransform(); + + p.setOpacity(_showProgress.current()); + st::historyRecordVoiceActive.paintInCenter(p, rect()); + + }, lifetime()); + + rpl::merge( + shownValue(), + _showProgress.value() | rpl::map(hasProgress) + ) | rpl::start_with_next([=](bool show) { + setVisible(show); + setMouseTracking(show); + if (!show) { + _recordingLevel = anim::value(); + _recordingAnimation.stop(); + _showingLifetime.destroy(); + _showProgress = 0.; + } else { + if (!_recordingAnimation.animating()) { + _recordingAnimation.start(); + } + } + }, lifetime()); + + actives( + ) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](bool active) { + setPointerCursor(active); + }, lifetime()); +} + +rpl::producer VoiceRecordButton::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 VoiceRecordButton::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 VoiceRecordButton::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 VoiceRecordButton::requestPaintProgress(float64 progress) { + _showProgress = progress; + update(); +} + +void VoiceRecordButton::requestPaintColor(float64 progress) { + _colorProgress = progress; + update(); +} + +} // namespace HistoryView::Controls 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 new file mode 100644 index 000000000..e44573d81 --- /dev/null +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h @@ -0,0 +1,58 @@ +/* +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 "ui/abstract_button.h" +#include "ui/effects/animations.h" +#include "ui/rp_widget.h" + +namespace HistoryView::Controls { + +class RecordCircle; + +class VoiceRecordButton final : public Ui::AbstractButton { +public: + VoiceRecordButton( + not_null parent, + rpl::producer<> leaveWindowEventProducer); + ~VoiceRecordButton(); + + void requestPaintColor(float64 progress); + void requestPaintProgress(float64 progress); + void requestPaintLevel(quint16 level); + + [[nodiscard]] rpl::producer actives() const; + + [[nodiscard]] bool inCircle(const QPoint &localPos) const; + +private: + void init(); + + void drawProgress(Painter &p); + + rpl::event_stream _recordAnimationTicked; + std::unique_ptr _recordCircle; + + 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; +}; + +} // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index bd386eb65..ebaca8649 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -342,9 +342,15 @@ historyRecordFont: font(13px); historyRecordDurationSkip: 12px; historyRecordDurationFg: historyComposeAreaFg; -historyRecordLevelMainRadius: 37px; +historyRecordLevelMainRadius: 23px; +historyRecordLevelMainRadiusAmplitude: 14px; +historyRecordMajorAmplitudeRadius: 14px; +historyRecordMinorAmplitudeRadius: 7px; +historyRecordRandomAddition: 8px; +historyRecordRadiusDiff: 50px; +historyRecordRadiusDiffMin: 10px; historyRecordLevelMinRadius: 38px; -historyRecordLevelMaxRadius: 60px; +historyRecordLevelMaxRadius: 70px; historyRecordTextStyle: TextStyle(defaultTextStyle) { font: historyRecordFont;