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_button.h"
9
10 #include "ui/paint/blobs.h"
11
12 #include "styles/style_chat.h"
13 #include "styles/style_layers.h"
14
15 namespace HistoryView::Controls {
16
17 namespace {
18
19 constexpr auto kMaxLevel = 1800.;
20 constexpr auto kBlobAlpha = 76. / 255.;
21 constexpr auto kBlobMaxSpeed = 5.0;
22 constexpr auto kLevelDuration = 100. + 500. * 0.33;
23 constexpr auto kBlobsScaleEnterDuration = crl::time(250);
24
Blobs()25 auto Blobs() {
26 return std::vector<Ui::Paint::Blobs::BlobData>{
27 {
28 .segmentsCount = 9,
29 .minScale = 0.605229,
30 .minRadius = (float)st::historyRecordMinorBlobMinRadius,
31 .maxRadius = (float)st::historyRecordMinorBlobMaxRadius,
32 .speedScale = 1.,
33 .alpha = kBlobAlpha,
34 .maxSpeed = kBlobMaxSpeed,
35 },
36 {
37 .segmentsCount = 12,
38 .minScale = 0.553943,
39 .minRadius = (float)st::historyRecordMajorBlobMinRadius,
40 .maxRadius = (float)st::historyRecordMajorBlobMaxRadius,
41 .speedScale = 1.,
42 .alpha = kBlobAlpha,
43 .maxSpeed = kBlobMaxSpeed,
44 },
45 };
46 }
47
48 } // namespace
49
VoiceRecordButton(not_null<Ui::RpWidget * > parent,rpl::producer<> leaveWindowEventProducer)50 VoiceRecordButton::VoiceRecordButton(
51 not_null<Ui::RpWidget*> parent,
52 rpl::producer<> leaveWindowEventProducer)
53 : AbstractButton(parent)
54 , _blobs(std::make_unique<Ui::Paint::Blobs>(
55 Blobs(),
56 kLevelDuration,
57 kMaxLevel))
58 , _center(_blobs->maxRadius()) {
59 resize(_center * 2, _center * 2);
60 std::move(
61 leaveWindowEventProducer
62 ) | rpl::start_with_next([=] {
63 _inCircle = false;
64 }, lifetime());
65 init();
66 }
67
68 VoiceRecordButton::~VoiceRecordButton() = default;
69
requestPaintLevel(quint16 level)70 void VoiceRecordButton::requestPaintLevel(quint16 level) {
71 if (_blobsHideLastTime) {
72 return;
73 }
74 _blobs->setLevel(level);
75 update();
76 }
77
init()78 void VoiceRecordButton::init() {
79 const auto currentState = lifetime().make_state<Type>(_state.current());
80
81 rpl::single(
82 anim::Disabled()
83 ) | rpl::then(
84 anim::Disables()
85 ) | rpl::start_with_next([=](bool hide) {
86 if (hide) {
87 _blobs->setLevel(0.);
88 }
89 _blobsHideLastTime = hide ? crl::now() : 0;
90 if (!hide && !_animation.animating() && isVisible()) {
91 _animation.start();
92 }
93 }, lifetime());
94
95 const auto &mainRadiusMin = st::historyRecordMainBlobMinRadius;
96 const auto mainRadiusDiff = st::historyRecordMainBlobMaxRadius
97 - mainRadiusMin;
98 paintRequest(
99 ) | rpl::start_with_next([=](const QRect &clip) {
100 Painter p(this);
101
102 const auto hideProgress = _blobsHideLastTime
103 ? 1. - std::clamp(
104 ((crl::now() - _blobsHideLastTime)
105 / (float64)kBlobsScaleEnterDuration),
106 0.,
107 1.)
108 : 1.;
109 const auto showProgress = _showProgress.current();
110 const auto complete = (showProgress == 1.);
111
112 p.translate(_center, _center);
113 PainterHighQualityEnabler hq(p);
114 const auto brush = QBrush(anim::color(
115 st::historyRecordVoiceFgInactive,
116 st::historyRecordVoiceFgActive,
117 _colorProgress));
118
119 _blobs->paint(p, brush, showProgress * hideProgress);
120
121 const auto radius = (mainRadiusMin
122 + (mainRadiusDiff * _blobs->currentLevel())) * showProgress;
123
124 p.setPen(Qt::NoPen);
125 p.setBrush(brush);
126 p.drawEllipse(QPointF(), radius, radius);
127
128 if (!complete) {
129 p.setOpacity(showProgress);
130 }
131
132 // Paint icon.
133 {
134 const auto stateProgress = _stateChangedAnimation.value(0.);
135 const auto scale = (std::cos(M_PI * 2 * stateProgress) + 1.) * .5;
136 if (scale < 1.) {
137 p.scale(scale, scale);
138 }
139 const auto state = *currentState;
140 const auto icon = (state == Type::Send)
141 ? st::historySendIcon
142 : st::historyRecordVoiceActive;
143 const auto position = (state == Type::Send)
144 ? st::historyRecordSendIconPosition
145 : QPoint(0, 0);
146 icon.paint(
147 p,
148 -icon.width() / 2 + position.x(),
149 -icon.height() / 2 + position.y(),
150 0,
151 st::historyRecordVoiceFgActiveIcon->c);
152 }
153 }, lifetime());
154
155 _animation.init([=](crl::time now) {
156 if (const auto &last = _blobsHideLastTime; (last > 0)
157 && (now - last >= kBlobsScaleEnterDuration)) {
158 _animation.stop();
159 return false;
160 }
161 _blobs->updateLevel(now - _lastUpdateTime);
162 _lastUpdateTime = now;
163 update();
164 return true;
165 });
166
167 rpl::merge(
168 shownValue(),
169 _showProgress.value(
170 ) | rpl::map(rpl::mappers::_1 != 0.) | rpl::distinct_until_changed()
171 ) | rpl::start_with_next([=](bool show) {
172 setVisible(show);
173 setMouseTracking(show);
174 if (!show) {
175 _animation.stop();
176 _showProgress = 0.;
177 _blobs->resetLevel();
178 _state = Type::Record;
179 } else {
180 if (!_animation.animating()) {
181 _animation.start();
182 }
183 }
184 }, lifetime());
185
186 actives(
187 ) | rpl::distinct_until_changed(
188 ) | rpl::start_with_next([=](bool active) {
189 setPointerCursor(active);
190 }, lifetime());
191
192 _state.changes(
193 ) | rpl::start_with_next([=](Type newState) {
194 const auto to = 1.;
195 auto callback = [=](float64 value) {
196 if (value >= (to * .5)) {
197 *currentState = newState;
198 }
199 update();
200 };
201 const auto duration = st::historyRecordVoiceDuration * 2;
202 _stateChangedAnimation.start(std::move(callback), 0., to, duration);
203 }, lifetime());
204 }
205
actives() const206 rpl::producer<bool> VoiceRecordButton::actives() const {
207 return events(
208 ) | rpl::filter([=](not_null<QEvent*> e) {
209 return (e->type() == QEvent::MouseMove
210 || e->type() == QEvent::Leave
211 || e->type() == QEvent::Enter);
212 }) | rpl::map([=](not_null<QEvent*> e) {
213 switch(e->type()) {
214 case QEvent::MouseMove:
215 return inCircle((static_cast<QMouseEvent*>(e.get()))->pos());
216 case QEvent::Leave: return false;
217 case QEvent::Enter: return inCircle(mapFromGlobal(QCursor::pos()));
218 default: return false;
219 }
220 });
221 }
222
clicks() const223 rpl::producer<> VoiceRecordButton::clicks() const {
224 return Ui::AbstractButton::clicks(
225 ) | rpl::to_empty | rpl::filter([=] {
226 return inCircle(mapFromGlobal(QCursor::pos()));
227 });
228 }
229
inCircle(const QPoint & localPos) const230 bool VoiceRecordButton::inCircle(const QPoint &localPos) const {
231 const auto &radii = st::historyRecordMainBlobMaxRadius;
232 const auto dx = std::abs(localPos.x() - _center);
233 if (dx > radii) {
234 return false;
235 }
236 const auto dy = std::abs(localPos.y() - _center);
237 if (dy > radii) {
238 return false;
239 } else if (dx + dy <= radii) {
240 return true;
241 }
242 return ((dx * dx + dy * dy) <= (radii * radii));
243 }
244
requestPaintProgress(float64 progress)245 void VoiceRecordButton::requestPaintProgress(float64 progress) {
246 _showProgress = progress;
247 update();
248 }
249
requestPaintColor(float64 progress)250 void VoiceRecordButton::requestPaintColor(float64 progress) {
251 if (_colorProgress == progress) {
252 return;
253 }
254 _colorProgress = progress;
255 update();
256 }
257
setType(Type state)258 void VoiceRecordButton::setType(Type state) {
259 _state = state;
260 }
261
262 } // namespace HistoryView::Controls
263