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