"Fossies" - the Fresh Open Source Software Archive

Member "tdesktop-2.6.0/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp" (23 Feb 2021, 44690 Bytes) of package /linux/misc/tdesktop-2.6.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "history_view_voice_record_bar.cpp" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 2.5.8_vs_2.5.9.

    1 /*
    2 This file is part of Telegram Desktop,
    3 the official desktop application for the Telegram messaging service.
    4 
    5 For license and copyright information please follow this link:
    6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
    7 */
    8 #include "history/view/controls/history_view_voice_record_bar.h"
    9 
   10 #include "api/api_send_progress.h"
   11 #include "base/event_filter.h"
   12 #include "base/openssl_help.h"
   13 #include "base/unixtime.h"
   14 #include "boxes/confirm_box.h"
   15 #include "core/application.h"
   16 #include "data/data_document.h"
   17 #include "data/data_document_media.h"
   18 #include "data/data_session.h"
   19 #include "history/history_item_components.h"
   20 #include "history/view/controls/history_view_voice_record_button.h"
   21 #include "lang/lang_keys.h"
   22 #include "main/main_session.h"
   23 #include "mainwidget.h" // MainWidget::stopAndClosePlayer
   24 #include "mainwindow.h"
   25 #include "media/audio/media_audio.h"
   26 #include "media/audio/media_audio_capture.h"
   27 #include "media/player/media_player_button.h"
   28 #include "media/player/media_player_instance.h"
   29 #include "styles/style_chat.h"
   30 #include "styles/style_layers.h"
   31 #include "styles/style_media_player.h"
   32 #include "ui/controls/send_button.h"
   33 #include "ui/effects/animation_value.h"
   34 #include "ui/effects/ripple_animation.h"
   35 #include "ui/text/format_values.h"
   36 #include "window/window_session_controller.h"
   37 
   38 namespace HistoryView::Controls {
   39 
   40 namespace {
   41 
   42 using SendActionUpdate = VoiceRecordBar::SendActionUpdate;
   43 using VoiceToSend = VoiceRecordBar::VoiceToSend;
   44 
   45 constexpr auto kAudioVoiceUpdateView = crl::time(200);
   46 constexpr auto kLockDelay = crl::time(100);
   47 constexpr auto kRecordingUpdateDelta = crl::time(100);
   48 constexpr auto kAudioVoiceMaxLength = 100 * 60; // 100 minutes
   49 constexpr auto kMaxSamples =
   50     ::Media::Player::kDefaultFrequency * kAudioVoiceMaxLength;
   51 
   52 constexpr auto kInactiveWaveformBarAlpha = int(255 * 0.6);
   53 
   54 constexpr auto kPrecision = 10;
   55 
   56 constexpr auto kLockArcAngle = 15.;
   57 
   58 constexpr auto kHideWaveformBgOffset = 50;
   59 
   60 enum class FilterType {
   61     Continue,
   62     ShowBox,
   63     Cancel,
   64 };
   65 
   66 [[nodiscard]] auto InactiveColor(const QColor &c) {
   67     return QColor(c.red(), c.green(), c.blue(), kInactiveWaveformBarAlpha);
   68 }
   69 
   70 [[nodiscard]] auto Progress(int low, int high) {
   71     return std::clamp(float64(low) / high, 0., 1.);
   72 }
   73 
   74 [[nodiscard]] auto Duration(int samples) {
   75     return samples / ::Media::Player::kDefaultFrequency;
   76 }
   77 
   78 [[nodiscard]] auto FormatVoiceDuration(int samples) {
   79     const int duration = kPrecision
   80         * (float64(samples) / ::Media::Player::kDefaultFrequency);
   81     const auto durationString = Ui::FormatDurationText(duration / kPrecision);
   82     const auto decimalPart = duration % kPrecision;
   83     return QString("%1%2%3")
   84         .arg(durationString)
   85         .arg(QLocale::system().decimalPoint())
   86         .arg(decimalPart);
   87 }
   88 
   89 [[nodiscard]] std::unique_ptr<VoiceData> ProcessCaptureResult(
   90         const ::Media::Capture::Result &data) {
   91     auto voiceData = std::make_unique<VoiceData>();
   92     voiceData->duration = Duration(data.samples);
   93     voiceData->waveform = data.waveform;
   94     voiceData->wavemax = voiceData->waveform.empty()
   95         ? uchar(0)
   96         : *ranges::max_element(voiceData->waveform);
   97     return voiceData;
   98 }
   99 
  100 [[nodiscard]] not_null<DocumentData*> DummyDocument(
  101         not_null<Data::Session*> owner) {
  102     return owner->document(
  103         openssl::RandomValue<DocumentId>(),
  104         uint64(0),
  105         QByteArray(),
  106         base::unixtime::now(),
  107         QVector<MTPDocumentAttribute>(),
  108         QString(),
  109         QByteArray(),
  110         ImageWithLocation(),
  111         ImageWithLocation(),
  112         owner->session().mainDcId(),
  113         int32(0));
  114 }
  115 
  116 void PaintWaveform(
  117         Painter &p,
  118         not_null<const VoiceData*> voiceData,
  119         int availableWidth,
  120         const QColor &active,
  121         const QColor &inactive,
  122         float64 progress) {
  123     const auto wf = [&]() -> const VoiceWaveform* {
  124         if (voiceData->waveform.isEmpty()) {
  125             return nullptr;
  126         } else if (voiceData->waveform.at(0) < 0) {
  127             return nullptr;
  128         }
  129         return &voiceData->waveform;
  130     }();
  131 
  132     const auto samplesCount = wf
  133         ? wf->size()
  134         : ::Media::Player::kWaveformSamplesCount;
  135     const auto activeWidth = std::round(availableWidth * progress);
  136 
  137     const auto &barWidth = st::historyRecordWaveformBar;
  138     const auto barFullWidth = barWidth + st::msgWaveformSkip;
  139     const auto totalBarsCountF = (float)availableWidth / barFullWidth;
  140     const auto totalBarsCount = int(totalBarsCountF);
  141     const auto samplesPerBar = samplesCount / totalBarsCountF;
  142     const auto barNormValue = (wf ? voiceData->wavemax : 0) + 1;
  143     const auto maxDelta = st::msgWaveformMax - st::msgWaveformMin;
  144     const auto &bottom = st::msgWaveformMax;
  145 
  146     p.setPen(Qt::NoPen);
  147     int barNum = 0;
  148     const auto paintBar = [&](const auto &barValue) {
  149         const auto barHeight = st::msgWaveformMin + barValue;
  150         const auto barTop = (bottom - barHeight) / 2.;
  151         const auto barLeft = barNum * barFullWidth;
  152         const auto rect = [&](const auto &l, const auto &w) {
  153             return QRectF(l, barTop, w, barHeight);
  154         };
  155 
  156         if ((barLeft < activeWidth) && (barLeft + barWidth > activeWidth)) {
  157             const auto leftWidth = activeWidth - barLeft;
  158             const auto rightWidth = barWidth - leftWidth;
  159             p.fillRect(rect(barLeft, leftWidth), active);
  160             p.fillRect(rect(activeWidth, rightWidth), inactive);
  161         } else {
  162             const auto &color = (barLeft >= activeWidth) ? inactive : active;
  163             p.fillRect(rect(barLeft, barWidth), color);
  164         }
  165         barNum++;
  166     };
  167 
  168     auto barCounter = 0.;
  169     auto nextBarNum = 0;
  170 
  171     auto sum = 0;
  172     auto maxValue = 0;
  173 
  174     for (auto i = 0; i < samplesCount; i++) {
  175         const auto value = wf ? wf->at(i) : 0;
  176         if (i != nextBarNum) {
  177             maxValue = std::max(maxValue, value);
  178             sum += totalBarsCount;
  179             continue;
  180         }
  181 
  182         // Compute height.
  183         sum += totalBarsCount - samplesCount;
  184         const auto isSumSmaller = (sum < (totalBarsCount + 1) / 2);
  185         if (isSumSmaller) {
  186             maxValue = std::max(maxValue, value);
  187         }
  188         const auto barValue = ((maxValue * maxDelta) + (barNormValue / 2))
  189             / barNormValue;
  190         maxValue = isSumSmaller ? 0 : value;
  191 
  192         const auto lastBarNum = nextBarNum;
  193         while (lastBarNum == nextBarNum) {
  194             barCounter += samplesPerBar;
  195             nextBarNum = (int)barCounter;
  196             paintBar(barValue);
  197         }
  198     }
  199 }
  200 
  201 } // namespace
  202 
  203 class ListenWrap final {
  204 public:
  205     ListenWrap(
  206         not_null<Ui::RpWidget*> parent,
  207         not_null<Window::SessionController*> controller,
  208         ::Media::Capture::Result &&data,
  209         const style::font &font);
  210 
  211     void requestPaintProgress(float64 progress);
  212     rpl::producer<> stopRequests() const;
  213     ::Media::Capture::Result *data() const;
  214 
  215     void playPause();
  216 
  217     rpl::lifetime &lifetime();
  218 
  219 private:
  220     void init();
  221     void initPlayButton();
  222     void initPlayProgress();
  223 
  224     bool isInPlayer(const ::Media::Player::TrackState &state) const;
  225     bool isInPlayer() const;
  226 
  227     int computeTopMargin(int height) const;
  228     QRect computeWaveformRect(const QRect &centerRect) const;
  229 
  230     not_null<Ui::RpWidget*> _parent;
  231 
  232     const not_null<Window::SessionController*> _controller;
  233     const not_null<DocumentData*> _document;
  234     const std::unique_ptr<VoiceData> _voiceData;
  235     const std::shared_ptr<Data::DocumentMedia> _mediaView;
  236     const std::unique_ptr<::Media::Capture::Result> _data;
  237     const style::IconButton &_stDelete;
  238     const base::unique_qptr<Ui::IconButton> _delete;
  239     const style::font &_durationFont;
  240     const QString _duration;
  241     const int _durationWidth;
  242     const style::MediaPlayerButton &_playPauseSt;
  243     const base::unique_qptr<Ui::AbstractButton> _playPauseButton;
  244     const QColor _activeWaveformBar;
  245     const QColor _inactiveWaveformBar;
  246 
  247     bool _isShowAnimation = true;
  248 
  249     QRect _waveformBgRect;
  250     QRect _waveformBgFinalCenterRect;
  251     QRect _waveformFgRect;
  252 
  253     ::Media::Player::PlayButtonLayout _playPause;
  254 
  255     anim::value _playProgress;
  256 
  257     rpl::variable<float64> _showProgress = 0.;
  258 
  259     rpl::lifetime _lifetime;
  260 
  261 };
  262 
  263 ListenWrap::ListenWrap(
  264     not_null<Ui::RpWidget*> parent,
  265     not_null<Window::SessionController*> controller,
  266     ::Media::Capture::Result &&data,
  267     const style::font &font)
  268 : _parent(parent)
  269 , _controller(controller)
  270 , _document(DummyDocument(&_controller->session().data()))
  271 , _voiceData(ProcessCaptureResult(data))
  272 , _mediaView(_document->createMediaView())
  273 , _data(std::make_unique<::Media::Capture::Result>(std::move(data)))
  274 , _stDelete(st::historyRecordDelete)
  275 , _delete(base::make_unique_q<Ui::IconButton>(parent, _stDelete))
  276 , _durationFont(font)
  277 , _duration(Ui::FormatDurationText(
  278     float64(_data->samples) / ::Media::Player::kDefaultFrequency))
  279 , _durationWidth(_durationFont->width(_duration))
  280 , _playPauseSt(st::mediaPlayerButton)
  281 , _playPauseButton(base::make_unique_q<Ui::AbstractButton>(parent))
  282 , _activeWaveformBar(st::historyRecordVoiceFgActiveIcon->c)
  283 , _inactiveWaveformBar(InactiveColor(_activeWaveformBar))
  284 , _playPause(_playPauseSt, [=] { _playPauseButton->update(); }) {
  285     init();
  286 }
  287 
  288 void ListenWrap::init() {
  289     auto deleteShow = _showProgress.value(
  290     ) | rpl::map([](auto value) {
  291         return value == 1.;
  292     }) | rpl::distinct_until_changed();
  293     _delete->showOn(std::move(deleteShow));
  294 
  295     _parent->sizeValue(
  296     ) | rpl::start_with_next([=](QSize size) {
  297         _waveformBgRect = QRect({ 0, 0 }, size)
  298             .marginsRemoved(st::historyRecordWaveformBgMargins);
  299         {
  300             const auto m = _stDelete.width + _waveformBgRect.height() / 2;
  301             _waveformBgFinalCenterRect = _waveformBgRect.marginsRemoved(
  302                 style::margins(m, 0, m, 0));
  303         }
  304         {
  305             const auto &play = _playPauseSt.playOuter;
  306             const auto &final = _waveformBgFinalCenterRect;
  307             _playPauseButton->moveToLeft(
  308                 final.x() - (final.height() - play.width()) / 2,
  309                 final.y());
  310         }
  311         _waveformFgRect = computeWaveformRect(_waveformBgFinalCenterRect);
  312     }, _lifetime);
  313 
  314     _parent->paintRequest(
  315     ) | rpl::start_with_next([=](const QRect &clip) {
  316         Painter p(_parent);
  317         PainterHighQualityEnabler hq(p);
  318         const auto progress = _showProgress.current();
  319         p.setOpacity(progress);
  320         if (progress > 0. && progress < 1.) {
  321             _stDelete.icon.paint(p, _stDelete.iconPosition, _parent->width());
  322         }
  323 
  324         {
  325             const auto hideOffset = _isShowAnimation
  326                 ? 0
  327                 : anim::interpolate(kHideWaveformBgOffset, 0, progress);
  328             const auto deleteIconLeft = _stDelete.iconPosition.x();
  329             const auto bgRectRight = anim::interpolate(
  330                 deleteIconLeft,
  331                 _stDelete.width,
  332                 _isShowAnimation ? progress : 1.);
  333             const auto bgRectLeft = anim::interpolate(
  334                 _parent->width() - deleteIconLeft - _waveformBgRect.height(),
  335                 _stDelete.width,
  336                 _isShowAnimation ? progress : 1.);
  337             const auto bgRectMargins = style::margins(
  338                 bgRectLeft - hideOffset,
  339                 0,
  340                 bgRectRight + hideOffset,
  341                 0);
  342             const auto bgRect = _waveformBgRect.marginsRemoved(bgRectMargins);
  343 
  344             const auto horizontalMargin = bgRect.width() - bgRect.height();
  345             const auto bgLeftCircleRect = bgRect.marginsRemoved(
  346                 style::margins(0, 0, horizontalMargin, 0));
  347             const auto bgRightCircleRect = bgRect.marginsRemoved(
  348                 style::margins(horizontalMargin, 0, 0, 0));
  349 
  350             const auto halfHeight = bgRect.height() / 2;
  351             const auto bgCenterRect = bgRect.marginsRemoved(
  352                 style::margins(halfHeight, 0, halfHeight, 0));
  353 
  354             if (!_isShowAnimation) {
  355                 p.setOpacity(progress);
  356             }
  357             p.setPen(Qt::NoPen);
  358             p.setBrush(st::historyRecordCancelActive);
  359             QPainterPath path;
  360             path.setFillRule(Qt::WindingFill);
  361             path.addEllipse(bgLeftCircleRect);
  362             path.addEllipse(bgRightCircleRect);
  363             path.addRect(bgCenterRect);
  364             p.drawPath(path);
  365 
  366             // Duration paint.
  367             {
  368                 p.setFont(_durationFont);
  369                 p.setPen(st::historyRecordVoiceFgActiveIcon);
  370 
  371                 const auto top = computeTopMargin(_durationFont->ascent);
  372                 const auto rect = bgCenterRect.marginsRemoved(
  373                     style::margins(
  374                         bgCenterRect.width() - _durationWidth,
  375                         top,
  376                         0,
  377                         top));
  378                 p.drawText(rect, style::al_left, _duration);
  379             }
  380 
  381             // Waveform paint.
  382             {
  383                 const auto rect = (progress == 1.)
  384                     ? _waveformFgRect
  385                     : computeWaveformRect(bgCenterRect);
  386                 if (rect.width() > 0) {
  387                     p.translate(rect.topLeft());
  388                     PaintWaveform(
  389                         p,
  390                         _voiceData.get(),
  391                         rect.width(),
  392                         _activeWaveformBar,
  393                         _inactiveWaveformBar,
  394                         _playProgress.current());
  395                     p.resetTransform();
  396                 }
  397             }
  398         }
  399     }, _lifetime);
  400 
  401     initPlayButton();
  402     initPlayProgress();
  403 }
  404 
  405 void ListenWrap::initPlayButton() {
  406     using namespace ::Media::Player;
  407     using State = TrackState;
  408 
  409     _mediaView->setBytes(_data->bytes);
  410     _document->type = VoiceDocument;
  411 
  412     const auto &play = _playPauseSt.playOuter;
  413     const auto &width = _waveformBgFinalCenterRect.height();
  414     _playPauseButton->resize(width, width);
  415     _playPauseButton->show();
  416 
  417     _playPauseButton->paintRequest(
  418     ) | rpl::start_with_next([=](const QRect &clip) {
  419         Painter p(_playPauseButton);
  420 
  421         const auto progress = _showProgress.current();
  422         p.translate(width / 2, width / 2);
  423         if (progress < 1.) {
  424             p.scale(progress, progress);
  425         }
  426         p.translate(-play.width() / 2, -play.height() / 2);
  427         _playPause.paint(p, st::historyRecordVoiceFgActiveIcon);
  428     }, _playPauseButton->lifetime());
  429 
  430     _playPauseButton->setClickedCallback([=] {
  431         instance()->playPause({ _document, FullMsgId() });
  432     });
  433 
  434     const auto showPause = _lifetime.make_state<rpl::variable<bool>>(false);
  435     showPause->changes(
  436     ) | rpl::start_with_next([=](bool pause) {
  437         _playPause.setState(pause
  438             ? PlayButtonLayout::State::Pause
  439             : PlayButtonLayout::State::Play);
  440     }, _lifetime);
  441 
  442     instance()->updatedNotifier(
  443     ) | rpl::start_with_next([=](const State &state) {
  444         if (isInPlayer(state)) {
  445             *showPause = ShowPauseIcon(state.state);
  446         } else if (showPause->current()) {
  447             *showPause = false;
  448         }
  449     }, _lifetime);
  450 
  451     instance()->stops(
  452         AudioMsgId::Type::Voice
  453     ) | rpl::start_with_next([=] {
  454         *showPause = false;
  455     }, _lifetime);
  456 
  457     const auto weak = Ui::MakeWeak(_controller->content().get());
  458     _lifetime.add([=] {
  459         if (weak && isInPlayer()) {
  460             weak->stopAndClosePlayer();
  461         }
  462     });
  463 }
  464 
  465 void ListenWrap::initPlayProgress() {
  466     using namespace ::Media::Player;
  467     using State = TrackState;
  468 
  469     const auto animation = _lifetime.make_state<Ui::Animations::Basic>();
  470     const auto isPointer = _lifetime.make_state<rpl::variable<bool>>(false);
  471     const auto &voice = AudioMsgId::Type::Voice;
  472 
  473     const auto updateCursor = [=](const QPoint &p) {
  474         *isPointer = isInPlayer() ? _waveformFgRect.contains(p) : false;
  475     };
  476 
  477     rpl::merge(
  478         instance()->startsPlay(voice) | rpl::map_to(true),
  479         instance()->stops(voice) | rpl::map_to(false)
  480     ) | rpl::start_with_next([=](bool play) {
  481         _parent->setMouseTracking(isInPlayer() && play);
  482         updateCursor(_parent->mapFromGlobal(QCursor::pos()));
  483     }, _lifetime);
  484 
  485     instance()->updatedNotifier(
  486     ) | rpl::start_with_next([=](const State &state) {
  487         if (!isInPlayer(state)) {
  488             return;
  489         }
  490         const auto progress = state.length
  491             ? Progress(state.position, state.length)
  492             : 0.;
  493         if (IsStopped(state.state)) {
  494             _playProgress = anim::value();
  495         } else {
  496             _playProgress.start(progress);
  497         }
  498         animation->start();
  499     }, _lifetime);
  500 
  501     auto animationCallback = [=](crl::time now) {
  502         if (anim::Disabled()) {
  503             now += kAudioVoiceUpdateView;
  504         }
  505 
  506         const auto dt = (now - animation->started())
  507             / float64(kAudioVoiceUpdateView);
  508         if (dt >= 1.) {
  509             animation->stop();
  510             _playProgress.finish();
  511         } else {
  512             _playProgress.update(std::min(dt, 1.), anim::linear);
  513         }
  514         _parent->update(_waveformFgRect);
  515         return (dt < 1.);
  516     };
  517     animation->init(std::move(animationCallback));
  518 
  519     const auto isPressed = _lifetime.make_state<bool>(false);
  520 
  521     isPointer->changes(
  522     ) | rpl::start_with_next([=](bool pointer) {
  523         _parent->setCursor(pointer ? style::cur_pointer : style::cur_default);
  524     }, _lifetime);
  525 
  526     _parent->events(
  527     ) | rpl::filter([=](not_null<QEvent*> e) {
  528         return (e->type() == QEvent::MouseMove
  529             || e->type() == QEvent::MouseButtonPress
  530             || e->type() == QEvent::MouseButtonRelease);
  531     }) | rpl::start_with_next([=](not_null<QEvent*> e) {
  532         if (!isInPlayer()) {
  533             return;
  534         }
  535 
  536         const auto type = e->type();
  537         const auto isMove = (type == QEvent::MouseMove);
  538         const auto &pos = static_cast<QMouseEvent*>(e.get())->pos();
  539         if (*isPressed) {
  540             *isPointer = true;
  541         } else if (isMove) {
  542             updateCursor(pos);
  543         }
  544         if (type == QEvent::MouseButtonPress) {
  545             if (isPointer->current() && !(*isPressed)) {
  546                 instance()->startSeeking(voice);
  547                 *isPressed = true;
  548             }
  549         } else if (*isPressed) {
  550             const auto &rect = _waveformFgRect;
  551             const auto left = float64(pos.x() - rect.x());
  552             const auto progress = Progress(left, rect.width());
  553             const auto isRelease = (type == QEvent::MouseButtonRelease);
  554             if (isRelease || isMove) {
  555                 _playProgress = anim::value(progress, progress);
  556                 _parent->update(_waveformFgRect);
  557                 if (isRelease) {
  558                     instance()->finishSeeking(voice, progress);
  559                     *isPressed = false;
  560                 }
  561             }
  562         }
  563 
  564     }, _lifetime);
  565 }
  566 
  567 
  568 bool ListenWrap::isInPlayer(const ::Media::Player::TrackState &state) const {
  569     return (state.id && (state.id.audio() == _document));
  570 }
  571 
  572 bool ListenWrap::isInPlayer() const {
  573     using Type = AudioMsgId::Type;
  574     return isInPlayer(::Media::Player::instance()->getState(Type::Voice));
  575 }
  576 
  577 void ListenWrap::playPause() {
  578     _playPauseButton->clicked(Qt::NoModifier, Qt::LeftButton);
  579 }
  580 
  581 QRect ListenWrap::computeWaveformRect(const QRect &centerRect) const {
  582     const auto top = computeTopMargin(st::msgWaveformMax);
  583     const auto left = (_playPauseSt.playOuter.width() + centerRect.height())
  584         / 2;
  585     const auto right = st::historyRecordWaveformRightSkip + _durationWidth;
  586     return centerRect.marginsRemoved(style::margins(left, top, right, top));
  587 }
  588 
  589 int ListenWrap::computeTopMargin(int height) const {
  590     return (_waveformBgRect.height() - height) / 2;
  591 }
  592 
  593 void ListenWrap::requestPaintProgress(float64 progress) {
  594     _isShowAnimation = (_showProgress.current() < progress);
  595     _showProgress = progress;
  596 }
  597 
  598 rpl::producer<> ListenWrap::stopRequests() const {
  599     return _delete->clicks() | rpl::to_empty;
  600 }
  601 
  602 ::Media::Capture::Result *ListenWrap::data() const {
  603     return _data.get();
  604 }
  605 
  606 rpl::lifetime &ListenWrap::lifetime() {
  607     return _lifetime;
  608 }
  609 
  610 class RecordLock final : public Ui::RippleButton {
  611 public:
  612     RecordLock(not_null<Ui::RpWidget*> parent);
  613 
  614     void requestPaintProgress(float64 progress);
  615     void requestPaintLockToStopProgress(float64 progress);
  616 
  617     [[nodiscard]] rpl::producer<> locks() const;
  618     [[nodiscard]] bool isLocked() const;
  619     [[nodiscard]] bool isStopState() const;
  620 
  621     [[nodiscard]] float64 lockToStopProgress() const;
  622 
  623 protected:
  624     QImage prepareRippleMask() const override;
  625     QPoint prepareRippleStartPosition() const override;
  626 
  627 private:
  628     void init();
  629 
  630     void drawProgress(Painter &p);
  631     void setProgress(float64 progress);
  632     void startLockingAnimation(float64 to);
  633 
  634     const QRect _rippleRect;
  635     const QPen _arcPen;
  636 
  637     Ui::Animations::Simple _lockEnderAnimation;
  638 
  639     float64 _lockToStopProgress = 0.;
  640     rpl::variable<float64> _progress = 0.;
  641 };
  642 
  643 RecordLock::RecordLock(not_null<Ui::RpWidget*> parent)
  644 : RippleButton(parent, st::defaultRippleAnimation)
  645 , _rippleRect(QRect(
  646     0,
  647     0,
  648     st::historyRecordLockTopShadow.width(),
  649     st::historyRecordLockTopShadow.width())
  650         .marginsRemoved(st::historyRecordLockRippleMargin))
  651 , _arcPen(
  652     st::historyRecordLockIconFg,
  653     st::historyRecordLockIconLineWidth,
  654     Qt::SolidLine,
  655     Qt::SquareCap,
  656     Qt::RoundJoin) {
  657     init();
  658 }
  659 
  660 void RecordLock::init() {
  661     shownValue(
  662     ) | rpl::start_with_next([=](bool shown) {
  663         resize(
  664             st::historyRecordLockTopShadow.width(),
  665             st::historyRecordLockSize.height());
  666         if (!shown) {
  667             setCursor(style::cur_default);
  668             setAttribute(Qt::WA_TransparentForMouseEvents, true);
  669             _lockEnderAnimation.stop();
  670             _lockToStopProgress = 0.;
  671             _progress = 0.;
  672         }
  673     }, lifetime());
  674 
  675     paintRequest(
  676     ) | rpl::start_with_next([=](const QRect &clip) {
  677         Painter p(this);
  678         if (isLocked()) {
  679             const auto top = anim::interpolate(
  680                 0,
  681                 height() - st::historyRecordLockTopShadow.height() * 2,
  682                 _lockToStopProgress);
  683             p.translate(0, top);
  684             drawProgress(p);
  685             return;
  686         }
  687         drawProgress(p);
  688     }, lifetime());
  689 }
  690 
  691 void RecordLock::drawProgress(Painter &p) {
  692     const auto progress = _progress.current();
  693 
  694     const auto &originTop = st::historyRecordLockTop;
  695     const auto &originBottom = st::historyRecordLockBottom;
  696     const auto &originBody = st::historyRecordLockBody;
  697     const auto &shadowTop = st::historyRecordLockTopShadow;
  698     const auto &shadowBottom = st::historyRecordLockBottomShadow;
  699     const auto &shadowBody = st::historyRecordLockBodyShadow;
  700     const auto &shadowMargins = st::historyRecordLockMargin;
  701 
  702     const auto bottomMargin = anim::interpolate(
  703         0,
  704         rect().height() - shadowTop.height() - shadowBottom.height(),
  705         progress);
  706 
  707     const auto topMargin = anim::interpolate(
  708         rect().height() / 4,
  709         0,
  710         progress);
  711 
  712     const auto full = rect().marginsRemoved(
  713         style::margins(0, topMargin, 0, bottomMargin));
  714     const auto inner = full.marginsRemoved(shadowMargins);
  715     const auto content = inner.marginsRemoved(style::margins(
  716         0,
  717         originTop.height(),
  718         0,
  719         originBottom.height()));
  720     const auto contentShadow = full.marginsRemoved(style::margins(
  721         0,
  722         shadowTop.height(),
  723         0,
  724         shadowBottom.height()));
  725 
  726     const auto w = full.width();
  727     {
  728         shadowTop.paint(p, full.topLeft(), w);
  729         originTop.paint(p, inner.topLeft(), w);
  730     }
  731     {
  732         const auto shadowPos = QPoint(
  733             full.x(),
  734             contentShadow.y() + contentShadow.height());
  735         const auto originPos = QPoint(
  736             inner.x(),
  737             content.y() + content.height());
  738         shadowBottom.paint(p, shadowPos, w);
  739         originBottom.paint(p, originPos, w);
  740     }
  741     {
  742         shadowBody.fill(p, contentShadow);
  743         originBody.fill(p, content);
  744     }
  745     {
  746         const auto &arrow = st::historyRecordLockArrow;
  747         const auto arrowRect = QRect(
  748             inner.x(),
  749             content.y() + content.height() - arrow.height() / 2,
  750             inner.width(),
  751             arrow.height());
  752         p.setOpacity(1. - progress);
  753         arrow.paintInCenter(p, arrowRect);
  754         p.setOpacity(1.);
  755     }
  756     if (isLocked()) {
  757         paintRipple(p, _rippleRect.x(), _rippleRect.y());
  758     }
  759     {
  760         PainterHighQualityEnabler hq(p);
  761         const auto &arcOffset = st::historyRecordLockIconLineSkip;
  762         const auto &size = st::historyRecordLockIconSize;
  763 
  764         const auto arcWidth = size.width() - arcOffset * 2;
  765         const auto &arcHeight = st::historyRecordLockIconArcHeight;
  766 
  767         const auto &blockHeight = st::historyRecordLockIconBottomHeight;
  768 
  769         const auto blockRectWidth = anim::interpolateF(
  770             size.width(),
  771             st::historyRecordStopIconWidth,
  772             _lockToStopProgress);
  773         const auto blockRectHeight = anim::interpolateF(
  774             blockHeight,
  775             st::historyRecordStopIconWidth,
  776             _lockToStopProgress);
  777         const auto blockRectTop = anim::interpolateF(
  778             size.height() - blockHeight,
  779             std::round((size.height() - blockRectHeight) / 2.),
  780             _lockToStopProgress);
  781 
  782         const auto blockRect = QRectF(
  783             (size.width() - blockRectWidth) / 2,
  784             blockRectTop,
  785             blockRectWidth,
  786             blockRectHeight);
  787         const auto &lineHeight = st::historyRecordLockIconLineHeight;
  788 
  789         p.setPen(Qt::NoPen);
  790         p.setBrush(st::historyRecordLockIconFg);
  791         p.translate(
  792             inner.x() + (inner.width() - size.width()) / 2,
  793             inner.y() + (originTop.height() * 2 - size.height()) / 2);
  794         {
  795             const auto xRadius = anim::interpolate(2, 3, _lockToStopProgress);
  796             p.drawRoundedRect(blockRect, xRadius, 3);
  797         }
  798 
  799         const auto offsetTranslate = _lockToStopProgress *
  800             (lineHeight + arcHeight + _arcPen.width() * 2);
  801         p.translate(
  802             size.width() - arcOffset,
  803             blockRect.y() + offsetTranslate);
  804 
  805         if (progress < 1. && progress > 0.) {
  806             p.rotate(kLockArcAngle * progress);
  807         }
  808 
  809         p.setPen(_arcPen);
  810         const auto rLine = QLineF(0, 0, 0, -lineHeight);
  811         p.drawLine(rLine);
  812 
  813         p.drawArc(
  814             -arcWidth,
  815             rLine.dy() - arcHeight - _arcPen.width() + rLine.y1(),
  816             arcWidth,
  817             arcHeight * 2,
  818             0,
  819             180 * 16);
  820 
  821         const auto lockProgress = 1. - _lockToStopProgress;
  822         if (progress == 1. && lockProgress < 1.) {
  823             p.drawLine(
  824                 -arcWidth,
  825                 rLine.y2(),
  826                 -arcWidth,
  827                 rLine.dy() * lockProgress);
  828         }
  829     }
  830 }
  831 
  832 void RecordLock::startLockingAnimation(float64 to) {
  833     auto callback = [=](float64 value) { setProgress(value); };
  834     const auto &duration = st::historyRecordVoiceShowDuration;
  835     _lockEnderAnimation.start(std::move(callback), 0., to, duration);
  836 }
  837 
  838 void RecordLock::requestPaintProgress(float64 progress) {
  839     if (isHidden()
  840         || isLocked()
  841         || _lockEnderAnimation.animating()
  842         || (_progress.current() == progress)) {
  843         return;
  844     }
  845     if (!_progress.current() && (progress > .3)) {
  846         startLockingAnimation(progress);
  847         return;
  848     }
  849     setProgress(progress);
  850 }
  851 
  852 void RecordLock::requestPaintLockToStopProgress(float64 progress) {
  853     _lockToStopProgress = progress;
  854     if (isStopState()) {
  855         setCursor(style::cur_pointer);
  856         setAttribute(Qt::WA_TransparentForMouseEvents, false);
  857 
  858         resize(
  859             st::historyRecordLockTopShadow.width(),
  860             st::historyRecordLockTopShadow.width());
  861     }
  862     update();
  863 }
  864 
  865 float64 RecordLock::lockToStopProgress() const {
  866     return _lockToStopProgress;
  867 }
  868 
  869 void RecordLock::setProgress(float64 progress) {
  870     _progress = progress;
  871     update();
  872 }
  873 
  874 bool RecordLock::isLocked() const {
  875     return _progress.current() == 1.;
  876 }
  877 
  878 bool RecordLock::isStopState() const {
  879     return isLocked() && (_lockToStopProgress == 1.);
  880 }
  881 
  882 rpl::producer<> RecordLock::locks() const {
  883     return _progress.changes(
  884     ) | rpl::filter([=] { return isLocked(); }) | rpl::to_empty;
  885 }
  886 
  887 QImage RecordLock::prepareRippleMask() const {
  888     return Ui::RippleAnimation::ellipseMask(_rippleRect.size());
  889 }
  890 
  891 QPoint RecordLock::prepareRippleStartPosition() const {
  892     return mapFromGlobal(QCursor::pos()) - _rippleRect.topLeft();
  893 }
  894 
  895 class CancelButton final : public Ui::RippleButton {
  896 public:
  897     CancelButton(not_null<Ui::RpWidget*> parent, int height);
  898 
  899     void requestPaintProgress(float64 progress);
  900 
  901 protected:
  902     QImage prepareRippleMask() const override;
  903     QPoint prepareRippleStartPosition() const override;
  904 
  905 private:
  906     void init();
  907 
  908     const int _width;
  909     const QRect _rippleRect;
  910 
  911     rpl::variable<float64> _showProgress = 0.;
  912 
  913     Ui::Text::String _text;
  914 
  915 };
  916 
  917 CancelButton::CancelButton(not_null<Ui::RpWidget*> parent, int height)
  918 : Ui::RippleButton(parent, st::defaultLightButton.ripple)
  919 , _width(st::historyRecordCancelButtonWidth)
  920 , _rippleRect(QRect(0, (height - _width) / 2, _width, _width))
  921 , _text(st::semiboldTextStyle, tr::lng_selected_clear(tr::now).toUpper()) {
  922     resize(_width, height);
  923     init();
  924 }
  925 
  926 void CancelButton::init() {
  927     _showProgress.value(
  928     ) | rpl::map(rpl::mappers::_1 > 0.) | rpl::distinct_until_changed(
  929     ) | rpl::start_with_next([=](bool hasProgress) {
  930         setVisible(hasProgress);
  931     }, lifetime());
  932 
  933     paintRequest(
  934     ) | rpl::start_with_next([=] {
  935         Painter p(this);
  936 
  937         p.setOpacity(_showProgress.current());
  938 
  939         paintRipple(p, _rippleRect.x(), _rippleRect.y());
  940 
  941         p.setPen(st::historyRecordCancelButtonFg);
  942         _text.draw(
  943             p,
  944             0,
  945             (height() - _text.minHeight()) / 2,
  946             width(),
  947             style::al_center);
  948     }, lifetime());
  949 }
  950 
  951 QImage CancelButton::prepareRippleMask() const {
  952     return Ui::RippleAnimation::ellipseMask(_rippleRect.size());
  953 }
  954 
  955 QPoint CancelButton::prepareRippleStartPosition() const {
  956     return mapFromGlobal(QCursor::pos()) - _rippleRect.topLeft();
  957 }
  958 
  959 void CancelButton::requestPaintProgress(float64 progress) {
  960     _showProgress = progress;
  961     update();
  962 }
  963 
  964 VoiceRecordBar::VoiceRecordBar(
  965     not_null<Ui::RpWidget*> parent,
  966     not_null<Ui::RpWidget*> sectionWidget,
  967     not_null<Window::SessionController*> controller,
  968     std::shared_ptr<Ui::SendButton> send,
  969     int recorderHeight)
  970 : RpWidget(parent)
  971 , _sectionWidget(sectionWidget)
  972 , _controller(controller)
  973 , _send(send)
  974 , _lock(std::make_unique<RecordLock>(sectionWidget))
  975 , _level(std::make_unique<VoiceRecordButton>(
  976     sectionWidget,
  977     _controller->widget()->leaveEvents()))
  978 , _cancel(std::make_unique<CancelButton>(this, recorderHeight))
  979 , _startTimer([=] { startRecording(); })
  980 , _message(
  981     st::historyRecordTextStyle,
  982     tr::lng_record_cancel(tr::now),
  983     TextParseOptions{ TextParseMultiline, 0, 0, Qt::LayoutDirectionAuto })
  984 , _cancelFont(st::historyRecordFont) {
  985     resize(QSize(parent->width(), recorderHeight));
  986     init();
  987     hideFast();
  988 }
  989 
  990 VoiceRecordBar::VoiceRecordBar(
  991     not_null<Ui::RpWidget*> parent,
  992     not_null<Window::SessionController*> controller,
  993     std::shared_ptr<Ui::SendButton> send,
  994     int recorderHeight)
  995 : VoiceRecordBar(parent, parent, controller, send, recorderHeight) {
  996 }
  997 
  998 VoiceRecordBar::~VoiceRecordBar() {
  999     if (isRecording()) {
 1000         stopRecording(StopType::Cancel);
 1001     }
 1002 }
 1003 
 1004 void VoiceRecordBar::updateMessageGeometry() {
 1005     const auto left = _durationRect.x()
 1006         + _durationRect.width()
 1007         + st::historyRecordTextLeft;
 1008     const auto right = width()
 1009         - _send->width()
 1010         - st::historyRecordTextRight;
 1011     const auto textWidth = _message.maxWidth();
 1012     const auto width = ((right - left) < textWidth)
 1013         ? st::historyRecordTextWidthForWrap
 1014         : textWidth;
 1015     const auto countLines = std::ceil((float)textWidth / width);
 1016     const auto textHeight = _message.minHeight() * countLines;
 1017     _messageRect = QRect(
 1018         left + (right - left - width) / 2,
 1019         (height() - textHeight) / 2,
 1020         width,
 1021         textHeight);
 1022 }
 1023 
 1024 void VoiceRecordBar::updateLockGeometry() {
 1025     const auto right = anim::interpolate(
 1026         -_lock->width(),
 1027         st::historyRecordLockPosition.x(),
 1028         _showLockAnimation.value(_lockShowing.current() ? 1. : 0.));
 1029     _lock->moveToRight(right, _lock->y());
 1030 }
 1031 
 1032 void VoiceRecordBar::init() {
 1033     // Keep VoiceRecordBar behind SendButton.
 1034     rpl::single(
 1035     ) | rpl::then(
 1036         _send->events(
 1037         ) | rpl::filter([](not_null<QEvent*> e) {
 1038             return e->type() == QEvent::ZOrderChange;
 1039         }) | rpl::to_empty
 1040     ) | rpl::start_with_next([=] {
 1041         orderControls();
 1042     }, lifetime());
 1043 
 1044     shownValue(
 1045     ) | rpl::start_with_next([=](bool show) {
 1046         if (!show) {
 1047             finish();
 1048         }
 1049     }, lifetime());
 1050 
 1051     sizeValue(
 1052     ) | rpl::start_with_next([=](QSize size) {
 1053         _centerY = size.height() / 2;
 1054         {
 1055             const auto maxD = st::historyRecordSignalRadius * 2;
 1056             const auto point = _centerY - st::historyRecordSignalRadius;
 1057             _redCircleRect = { point, point, maxD, maxD };
 1058         }
 1059         {
 1060             const auto durationLeft = _redCircleRect.x()
 1061                 + _redCircleRect.width()
 1062                 + st::historyRecordDurationSkip;
 1063             const auto &ascent = _cancelFont->ascent;
 1064             _durationRect = QRect(
 1065                 durationLeft,
 1066                 _redCircleRect.y() - (ascent - _redCircleRect.height()) / 2,
 1067                 _cancelFont->width(FormatVoiceDuration(kMaxSamples)),
 1068                 ascent);
 1069         }
 1070         _cancel->moveToLeft((size.width() - _cancel->width()) / 2, 0);
 1071         updateMessageGeometry();
 1072         updateLockGeometry();
 1073     }, lifetime());
 1074 
 1075     paintRequest(
 1076     ) | rpl::start_with_next([=](const QRect &clip) {
 1077         Painter p(this);
 1078         if (_showAnimation.animating()) {
 1079             p.setOpacity(showAnimationRatio());
 1080         }
 1081         p.fillRect(clip, st::historyComposeAreaBg);
 1082 
 1083         p.setOpacity(std::min(p.opacity(), 1. - showListenAnimationRatio()));
 1084         const auto opacity = p.opacity();
 1085         _cancel->requestPaintProgress(_lock->isStopState()
 1086             ? (opacity * _lock->lockToStopProgress())
 1087             : 0.);
 1088 
 1089         if (!opacity) {
 1090             return;
 1091         }
 1092         if (clip.intersects(_messageRect)) {
 1093             // The message should be painted first to avoid flickering.
 1094             drawMessage(p, activeAnimationRatio());
 1095         }
 1096         if (clip.intersects(_durationRect)) {
 1097             drawDuration(p);
 1098         }
 1099         if (clip.intersects(_redCircleRect)) {
 1100             // Should be the last to be drawn.
 1101             drawRedCircle(p);
 1102         }
 1103     }, lifetime());
 1104 
 1105     _inField.changes(
 1106     ) | rpl::start_with_next([=](bool value) {
 1107         activeAnimate(value);
 1108     }, lifetime());
 1109 
 1110     _lockShowing.changes(
 1111     ) | rpl::start_with_next([=](bool show) {
 1112         const auto to = show ? 1. : 0.;
 1113         const auto from = show ? 0. : 1.;
 1114         const auto &duration = st::historyRecordLockShowDuration;
 1115         _lock->show();
 1116         auto callback = [=](float64 value) {
 1117             updateLockGeometry();
 1118             if (value == 0. && !show) {
 1119                 _lock->hide();
 1120             } else if (value == 1. && show) {
 1121                 computeAndSetLockProgress(QCursor::pos());
 1122             }
 1123         };
 1124         _showLockAnimation.start(std::move(callback), from, to, duration);
 1125     }, lifetime());
 1126 
 1127     _lock->setClickedCallback([=] {
 1128         if (!_lock->isStopState()) {
 1129             return;
 1130         }
 1131 
 1132         ::Media::Capture::instance()->startedChanges(
 1133         ) | rpl::filter([=](bool capturing) {
 1134             return !capturing && _listen;
 1135         }) | rpl::take(1) | rpl::start_with_next([=] {
 1136             _lockShowing = false;
 1137 
 1138             const auto to = 1.;
 1139             const auto &duration = st::historyRecordVoiceShowDuration;
 1140             auto callback = [=](float64 value) {
 1141                 _listen->requestPaintProgress(value);
 1142                 const auto reverseValue = to - value;
 1143                 _level->requestPaintProgress(reverseValue);
 1144                 update();
 1145                 if (to == value) {
 1146                     _recordingLifetime.destroy();
 1147                 }
 1148             };
 1149             _showListenAnimation.start(std::move(callback), 0., to, duration);
 1150         }, lifetime());
 1151 
 1152         stopRecording(StopType::Listen);
 1153     });
 1154 
 1155     _lock->locks(
 1156     ) | rpl::start_with_next([=] {
 1157         _level->setType(VoiceRecordButton::Type::Send);
 1158 
 1159         _level->clicks(
 1160         ) | rpl::start_with_next([=] {
 1161             stop(true);
 1162         }, _recordingLifetime);
 1163 
 1164         rpl::single(
 1165             false
 1166         ) | rpl::then(
 1167             _level->actives()
 1168         ) | rpl::start_with_next([=](bool enter) {
 1169             _inField = enter;
 1170         }, _recordingLifetime);
 1171 
 1172         const auto &duration = st::historyRecordVoiceShowDuration;
 1173         const auto from = 0.;
 1174         const auto to = 1.;
 1175         auto callback = [=](float64 value) {
 1176             _lock->requestPaintLockToStopProgress(value);
 1177             update();
 1178         };
 1179         _lockToStopAnimation.start(std::move(callback), from, to, duration);
 1180     }, lifetime());
 1181 
 1182     _send->events(
 1183     ) | rpl::filter([=](not_null<QEvent*> e) {
 1184         return isTypeRecord()
 1185             && !isRecording()
 1186             && !_showAnimation.animating()
 1187             && !_lock->isLocked()
 1188             && (e->type() == QEvent::MouseButtonPress
 1189                 || e->type() == QEvent::MouseButtonRelease);
 1190     }) | rpl::start_with_next([=](not_null<QEvent*> e) {
 1191         if (e->type() == QEvent::MouseButtonPress) {
 1192             if (_startRecordingFilter && _startRecordingFilter()) {
 1193                 return;
 1194             }
 1195             _startTimer.callOnce(st::historyRecordVoiceShowDuration);
 1196         } else if (e->type() == QEvent::MouseButtonRelease) {
 1197             _startTimer.cancel();
 1198         }
 1199     }, lifetime());
 1200 
 1201     _listenChanges.events(
 1202     ) | rpl::filter([=] {
 1203         return _listen != nullptr;
 1204     }) | rpl::start_with_next([=] {
 1205         _listen->stopRequests(
 1206         ) | rpl::take(1) | rpl::start_with_next([=] {
 1207             hideAnimated();
 1208         }, _listen->lifetime());
 1209 
 1210         _listen->lifetime().add([=] { _listenChanges.fire({}); });
 1211 
 1212         installListenStateFilter();
 1213     }, lifetime());
 1214 
 1215     _cancel->setClickedCallback([=] {
 1216         hideAnimated();
 1217     });
 1218 }
 1219 
 1220 void VoiceRecordBar::activeAnimate(bool active) {
 1221     const auto to = active ? 1. : 0.;
 1222     const auto &duration = st::historyRecordVoiceDuration;
 1223     if (_activeAnimation.animating()) {
 1224         _activeAnimation.change(to, duration);
 1225     } else {
 1226         auto callback = [=] {
 1227             update(_messageRect);
 1228             _level->requestPaintColor(activeAnimationRatio());
 1229         };
 1230         const auto from = active ? 0. : 1.;
 1231         _activeAnimation.start(std::move(callback), from, to, duration);
 1232     }
 1233 }
 1234 
 1235 void VoiceRecordBar::visibilityAnimate(bool show, Fn<void()> &&callback) {
 1236     const auto to = show ? 1. : 0.;
 1237     const auto from = show ? 0. : 1.;
 1238     const auto &duration = st::historyRecordVoiceShowDuration;
 1239     auto animationCallback = [=, callback = std::move(callback)](auto value) {
 1240         if (!_listen) {
 1241             _level->requestPaintProgress(value);
 1242         } else {
 1243             _listen->requestPaintProgress(value);
 1244         }
 1245         update();
 1246         if ((show && value == 1.) || (!show && value == 0.)) {
 1247             if (callback) {
 1248                 callback();
 1249             }
 1250         }
 1251     };
 1252     _showAnimation.start(std::move(animationCallback), from, to, duration);
 1253 }
 1254 
 1255 void VoiceRecordBar::setStartRecordingFilter(Fn<bool()> &&callback) {
 1256     _startRecordingFilter = std::move(callback);
 1257 }
 1258 
 1259 void VoiceRecordBar::setLockBottom(rpl::producer<int> &&bottom) {
 1260     rpl::combine(
 1261         std::move(bottom),
 1262         _lock->sizeValue() | rpl::map_to(true) // Dummy value.
 1263     ) | rpl::start_with_next([=](int value, bool dummy) {
 1264         _lock->moveToLeft(_lock->x(), value - _lock->height());
 1265     }, lifetime());
 1266 }
 1267 
 1268 void VoiceRecordBar::setSendButtonGeometryValue(
 1269         rpl::producer<QRect> &&geometry) {
 1270     std::move(
 1271         geometry
 1272     ) | rpl::start_with_next([=](QRect r) {
 1273         const auto center = (r.width() - _level->width()) / 2;
 1274         _level->moveToLeft(r.x() + center, r.y() + center);
 1275     }, lifetime());
 1276 }
 1277 
 1278 void VoiceRecordBar::startRecording() {
 1279     if (isRecording()) {
 1280         return;
 1281     }
 1282     auto appearanceCallback = [=] {
 1283         if(_showAnimation.animating()) {
 1284             return;
 1285         }
 1286 
 1287         using namespace ::Media::Capture;
 1288         if (!instance()->available()) {
 1289             stop(false);
 1290             return;
 1291         }
 1292 
 1293         _lockShowing = true;
 1294         startRedCircleAnimation();
 1295 
 1296         _recording = true;
 1297         _controller->widget()->setInnerFocus();
 1298         instance()->start();
 1299         instance()->updated(
 1300         ) | rpl::start_with_next_error([=](const Update &update) {
 1301             recordUpdated(update.level, update.samples);
 1302         }, [=] {
 1303             stop(false);
 1304         }, _recordingLifetime);
 1305         _recordingLifetime.add([=] {
 1306             _recording = false;
 1307         });
 1308     };
 1309     visibilityAnimate(true, std::move(appearanceCallback));
 1310     show();
 1311 
 1312     _inField = true;
 1313 
 1314     _send->events(
 1315     ) | rpl::filter([=](not_null<QEvent*> e) {
 1316         return isTypeRecord()
 1317             && !_lock->isLocked()
 1318             && (e->type() == QEvent::MouseMove
 1319                 || e->type() == QEvent::MouseButtonRelease);
 1320     }) | rpl::start_with_next([=](not_null<QEvent*> e) {
 1321         const auto type = e->type();
 1322         if (type == QEvent::MouseMove) {
 1323             const auto mouse = static_cast<QMouseEvent*>(e.get());
 1324             const auto globalPos = mouse->globalPos();
 1325             const auto localPos = mapFromGlobal(globalPos);
 1326             const auto inField = rect().contains(localPos);
 1327             _inField = inField
 1328                 ? inField
 1329                 : _level->inCircle(_level->mapFromGlobal(globalPos));
 1330 
 1331             if (_showLockAnimation.animating() || !hasDuration()) {
 1332                 return;
 1333             }
 1334             computeAndSetLockProgress(mouse->globalPos());
 1335         } else if (type == QEvent::MouseButtonRelease) {
 1336             stop(_inField.current());
 1337         }
 1338     }, _recordingLifetime);
 1339 }
 1340 
 1341 void VoiceRecordBar::recordUpdated(quint16 level, int samples) {
 1342     _level->requestPaintLevel(level);
 1343     _recordingSamples = samples;
 1344     if (samples < 0 || samples >= kMaxSamples) {
 1345         stop(samples > 0 && _inField.current());
 1346     }
 1347     Core::App().updateNonIdle();
 1348     update(_durationRect);
 1349     _sendActionUpdates.fire({ Api::SendProgressType::RecordVoice });
 1350 }
 1351 
 1352 void VoiceRecordBar::stop(bool send) {
 1353     if (isHidden() && !send) {
 1354         return;
 1355     }
 1356     auto disappearanceCallback = [=] {
 1357         hide();
 1358 
 1359         stopRecording(send ? StopType::Send : StopType::Cancel);
 1360     };
 1361     _lockShowing = false;
 1362     visibilityAnimate(false, std::move(disappearanceCallback));
 1363 }
 1364 
 1365 void VoiceRecordBar::finish() {
 1366     _recordingLifetime.destroy();
 1367     _lockShowing = false;
 1368     _inField = false;
 1369     _redCircleProgress = 0.;
 1370     _recordingSamples = 0;
 1371 
 1372     _showAnimation.stop();
 1373     _lockToStopAnimation.stop();
 1374 
 1375     _listen = nullptr;
 1376 
 1377     _sendActionUpdates.fire({ Api::SendProgressType::RecordVoice, -1 });
 1378     _controller->widget()->setInnerFocus();
 1379 }
 1380 
 1381 void VoiceRecordBar::hideFast() {
 1382     hide();
 1383     _lock->hide();
 1384     _level->hide();
 1385     stopRecording(StopType::Cancel);
 1386 }
 1387 
 1388 void VoiceRecordBar::stopRecording(StopType type) {
 1389     using namespace ::Media::Capture;
 1390     if (type == StopType::Cancel) {
 1391         instance()->stop(crl::guard(this, [=](Result &&data) {
 1392             _cancelRequests.fire({});
 1393         }));
 1394         return;
 1395     }
 1396     instance()->stop(crl::guard(this, [=](Result &&data) {
 1397         if (data.bytes.isEmpty()) {
 1398             // Close everything.
 1399             stop(false);
 1400             return;
 1401         }
 1402 
 1403         Window::ActivateWindow(_controller);
 1404         const auto duration = Duration(data.samples);
 1405         if (type == StopType::Send) {
 1406             _sendVoiceRequests.fire({ data.bytes, data.waveform, duration });
 1407         } else if (type == StopType::Listen) {
 1408             _listen = std::make_unique<ListenWrap>(
 1409                 this,
 1410                 _controller,
 1411                 std::move(data),
 1412                 _cancelFont);
 1413             _listenChanges.fire({});
 1414 
 1415             _lockShowing = false;
 1416         }
 1417     }));
 1418 }
 1419 
 1420 void VoiceRecordBar::drawDuration(Painter &p) {
 1421     const auto duration = FormatVoiceDuration(_recordingSamples);
 1422     p.setFont(_cancelFont);
 1423     p.setPen(st::historyRecordDurationFg);
 1424 
 1425     p.drawText(_durationRect, style::al_left, duration);
 1426 }
 1427 
 1428 void VoiceRecordBar::startRedCircleAnimation() {
 1429     if (anim::Disabled()) {
 1430         return;
 1431     }
 1432     const auto animation = _recordingLifetime
 1433         .make_state<Ui::Animations::Basic>();
 1434     animation->init([=](crl::time now) {
 1435         const auto diffTime = now - animation->started();
 1436         _redCircleProgress = std::abs(std::sin(diffTime / 400.));
 1437         update(_redCircleRect);
 1438         return true;
 1439     });
 1440     animation->start();
 1441 }
 1442 
 1443 void VoiceRecordBar::drawRedCircle(Painter &p) {
 1444     PainterHighQualityEnabler hq(p);
 1445     p.setPen(Qt::NoPen);
 1446     p.setBrush(st::historyRecordVoiceFgInactive);
 1447 
 1448     const auto opacity = p.opacity();
 1449     p.setOpacity(opacity * (1. - _redCircleProgress));
 1450     const int radii = st::historyRecordSignalRadius * showAnimationRatio();
 1451     const auto center = _redCircleRect.center() + QPoint(1, 1);
 1452     p.drawEllipse(center, radii, radii);
 1453     p.setOpacity(opacity);
 1454 }
 1455 
 1456 void VoiceRecordBar::drawMessage(Painter &p, float64 recordActive) {
 1457     p.setPen(
 1458         anim::pen(
 1459             st::historyRecordCancel,
 1460             st::historyRecordCancelActive,
 1461             1. - recordActive));
 1462 
 1463     const auto opacity = p.opacity();
 1464     p.setOpacity(opacity * (1. - _lock->lockToStopProgress()));
 1465 
 1466     _message.draw(
 1467         p,
 1468         _messageRect.x(),
 1469         _messageRect.y(),
 1470         _messageRect.width(),
 1471         style::al_center);
 1472 
 1473     p.setOpacity(opacity);
 1474 }
 1475 
 1476 void VoiceRecordBar::requestToSendWithOptions(Api::SendOptions options) {
 1477     if (isListenState()) {
 1478         const auto data = _listen->data();
 1479         _sendVoiceRequests.fire({
 1480             data->bytes,
 1481             data->waveform,
 1482             Duration(data->samples),
 1483             options });
 1484     }
 1485 }
 1486 
 1487 rpl::producer<SendActionUpdate> VoiceRecordBar::sendActionUpdates() const {
 1488     return _sendActionUpdates.events();
 1489 }
 1490 
 1491 rpl::producer<VoiceToSend> VoiceRecordBar::sendVoiceRequests() const {
 1492     return _sendVoiceRequests.events();
 1493 }
 1494 
 1495 rpl::producer<> VoiceRecordBar::cancelRequests() const {
 1496     return _cancelRequests.events();
 1497 }
 1498 
 1499 bool VoiceRecordBar::isRecording() const {
 1500     return _recording.current();
 1501 }
 1502 
 1503 bool VoiceRecordBar::isActive() const {
 1504     return isRecording() || isListenState();
 1505 }
 1506 
 1507 void VoiceRecordBar::hideAnimated() {
 1508     if (isHidden()) {
 1509         return;
 1510     }
 1511     _lockShowing = false;
 1512     visibilityAnimate(false, [=] { hideFast(); });
 1513 }
 1514 
 1515 void VoiceRecordBar::finishAnimating() {
 1516     _showAnimation.stop();
 1517 }
 1518 
 1519 rpl::producer<bool> VoiceRecordBar::recordingStateChanges() const {
 1520     return _recording.changes();
 1521 }
 1522 
 1523 rpl::producer<bool> VoiceRecordBar::lockShowStarts() const {
 1524     return _lockShowing.changes();
 1525 }
 1526 
 1527 rpl::producer<not_null<QEvent*>> VoiceRecordBar::lockViewportEvents() const {
 1528     return _lock->events(
 1529         ) | rpl::filter([=](not_null<QEvent*> e) {
 1530             return e->type() == QEvent::Wheel;
 1531         });
 1532 }
 1533 
 1534 rpl::producer<> VoiceRecordBar::updateSendButtonTypeRequests() const {
 1535     return _listenChanges.events();
 1536 }
 1537 
 1538 bool VoiceRecordBar::isLockPresent() const {
 1539     return _lockShowing.current();
 1540 }
 1541 
 1542 bool VoiceRecordBar::isListenState() const {
 1543     return _listen != nullptr;
 1544 }
 1545 
 1546 bool VoiceRecordBar::isTypeRecord() const {
 1547     return (_send->type() == Ui::SendButton::Type::Record);
 1548 }
 1549 
 1550 bool VoiceRecordBar::hasDuration() const {
 1551     return _recordingSamples > 0;
 1552 }
 1553 
 1554 float64 VoiceRecordBar::activeAnimationRatio() const {
 1555     return _activeAnimation.value(_inField.current() ? 1. : 0.);
 1556 }
 1557 
 1558 void VoiceRecordBar::clearListenState() {
 1559     if (isListenState()) {
 1560         hideAnimated();
 1561     }
 1562 }
 1563 
 1564 float64 VoiceRecordBar::showAnimationRatio() const {
 1565     // There is no reason to set the final value to zero,
 1566     // because at zero this widget is hidden.
 1567     return _showAnimation.value(1.);
 1568 }
 1569 
 1570 float64 VoiceRecordBar::showListenAnimationRatio() const {
 1571     return _showListenAnimation.value(_listen ? 1. : 0.);
 1572 }
 1573 
 1574 void VoiceRecordBar::computeAndSetLockProgress(QPoint globalPos) {
 1575     const auto localPos = mapFromGlobal(globalPos);
 1576     const auto lower = _lock->height();
 1577     const auto higher = 0;
 1578     _lock->requestPaintProgress(Progress(localPos.y(), higher - lower));
 1579 }
 1580 
 1581 void VoiceRecordBar::orderControls() {
 1582     stackUnder(_send.get());
 1583     _level->raise();
 1584     _lock->raise();
 1585 }
 1586 
 1587 void VoiceRecordBar::installListenStateFilter() {
 1588     auto keyFilterCallback = [=](not_null<QEvent*> e) {
 1589         using Result = base::EventFilterResult;
 1590         if (!(_send->type() == Ui::SendButton::Type::Send
 1591             || _send->type() == Ui::SendButton::Type::Schedule)) {
 1592             return Result::Continue;
 1593         }
 1594         switch(e->type()) {
 1595         case QEvent::KeyPress: {
 1596             const auto keyEvent = static_cast<QKeyEvent*>(e.get());
 1597             const auto key = keyEvent->key();
 1598             const auto isSpace = (key == Qt::Key_Space);
 1599             const auto isEnter = (key == Qt::Key_Enter
 1600                 || key == Qt::Key_Return);
 1601             if (isSpace && !keyEvent->isAutoRepeat() && _listen) {
 1602                 _listen->playPause();
 1603                 return Result::Cancel;
 1604             }
 1605             if (isEnter && !_warningShown) {
 1606                 requestToSendWithOptions({});
 1607                 return Result::Cancel;
 1608             }
 1609             return Result::Continue;
 1610         }
 1611         default: return Result::Continue;
 1612         }
 1613     };
 1614 
 1615     auto keyFilter = base::install_event_filter(
 1616         QCoreApplication::instance(),
 1617         std::move(keyFilterCallback));
 1618 
 1619     _listen->lifetime().make_state<base::unique_qptr<QObject>>(
 1620         std::move(keyFilter));
 1621 }
 1622 
 1623 void VoiceRecordBar::showDiscardBox(
 1624         Fn<void()> &&callback,
 1625         anim::type animated) {
 1626     if (!isActive()) {
 1627         return;
 1628     }
 1629     auto sure = [=, callback = std::move(callback)](Fn<void()> &&close) {
 1630         if (animated == anim::type::instant) {
 1631             hideFast();
 1632         } else {
 1633             hideAnimated();
 1634         }
 1635         close();
 1636         _warningShown = false;
 1637         if (callback) {
 1638             callback();
 1639         }
 1640     };
 1641     Ui::show(Box<ConfirmBox>(
 1642         (isListenState()
 1643             ? tr::lng_record_listen_cancel_sure
 1644             : tr::lng_record_lock_cancel_sure)(tr::now),
 1645         tr::lng_record_lock_discard(tr::now),
 1646         st::attentionBoxButton,
 1647         std::move(sure)));
 1648     _warningShown = true;
 1649 }
 1650 
 1651 } // namespace HistoryView::Controls