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 "ui/chat/attach/attach_album_thumbnail.h"
9 
10 #include "ui/chat/attach/attach_prepare.h"
11 #include "ui/image/image_prepare.h"
12 #include "ui/text/format_values.h"
13 #include "ui/widgets/buttons.h"
14 #include "ui/ui_utility.h"
15 #include "base/call_delayed.h"
16 #include "styles/style_chat.h"
17 #include "styles/style_boxes.h"
18 
19 #include <QtCore/QFileInfo>
20 
21 namespace Ui {
22 
AlbumThumbnail(const PreparedFile & file,const GroupMediaLayout & layout,QWidget * parent,Fn<void ()> editCallback,Fn<void ()> deleteCallback)23 AlbumThumbnail::AlbumThumbnail(
24 	const PreparedFile &file,
25 	const GroupMediaLayout &layout,
26 	QWidget *parent,
27 	Fn<void()> editCallback,
28 	Fn<void()> deleteCallback)
29 : _layout(layout)
30 , _fullPreview(file.preview)
31 , _shrinkSize(int(std::ceil(st::historyMessageRadius / 1.4)))
32 , _isPhoto(file.type == PreparedFile::Type::Photo)
33 , _isVideo(file.type == PreparedFile::Type::Video) {
34 	Expects(!_fullPreview.isNull());
35 
36 	moveToLayout(layout);
37 
38 	using Option = Images::Option;
39 	const auto previewWidth = _fullPreview.width();
40 	const auto previewHeight = _fullPreview.height();
41 	const auto imageWidth = std::max(
42 		previewWidth / style::DevicePixelRatio(),
43 		st::minPhotoSize);
44 	const auto imageHeight = std::max(
45 		previewHeight / style::DevicePixelRatio(),
46 		st::minPhotoSize);
47 	_photo = PixmapFromImage(Images::prepare(
48 		_fullPreview,
49 		previewWidth,
50 		previewHeight,
51 		Option::RoundedLarge | Option::RoundedAll,
52 		imageWidth,
53 		imageHeight));
54 
55 	const auto &st = st::attachPreviewThumbLayout;
56 	const auto idealSize = st.thumbSize * style::DevicePixelRatio();
57 	const auto fileThumbSize = (previewWidth > previewHeight)
58 		? QSize(previewWidth * idealSize / previewHeight, idealSize)
59 		: QSize(idealSize, previewHeight * idealSize / previewWidth);
60 	_fileThumb = PixmapFromImage(Images::prepare(
61 		_fullPreview,
62 		fileThumbSize.width(),
63 		fileThumbSize.height(),
64 		Option::RoundedSmall | Option::RoundedAll,
65 		st.thumbSize,
66 		st.thumbSize
67 	));
68 
69 	const auto availableFileWidth = st::sendMediaPreviewSize
70 		- st.thumbSize
71 		- st.padding.right()
72 		// Right buttons.
73 		- st::sendBoxAlbumGroupButtonFile.width * 2
74 		- st::sendBoxAlbumGroupEditInternalSkip * 2
75 		- st::sendBoxAlbumGroupSkipRight;
76 	const auto filepath = file.path;
77 	if (filepath.isEmpty()) {
78 		_name = "image.png";
79 		_status = FormatImageSizeText(_fullPreview.size()
80 			/ _fullPreview.devicePixelRatio());
81 	} else {
82 		auto fileinfo = QFileInfo(filepath);
83 		_name = fileinfo.fileName();
84 		_status = FormatSizeText(fileinfo.size());
85 	}
86 	_nameWidth = st::semiboldFont->width(_name);
87 	if (_nameWidth > availableFileWidth) {
88 		_name = st::semiboldFont->elided(
89 			_name,
90 			availableFileWidth,
91 			Qt::ElideMiddle);
92 		_nameWidth = st::semiboldFont->width(_name);
93 	}
94 	_statusWidth = st::normalFont->width(_status);
95 
96 	_editMedia.create(parent, st::sendBoxAlbumGroupButtonFile);
97 	_deleteMedia.create(parent, st::sendBoxAlbumGroupButtonFile);
98 
99 	const auto duration = st::historyAttach.ripple.hideDuration;
100 	_editMedia->setClickedCallback([=] {
101 		base::call_delayed(duration, parent, editCallback);
102 	});
103 	_deleteMedia->setClickedCallback(deleteCallback);
104 
105 	_editMedia->setIconOverride(&st::sendBoxAlbumGroupEditButtonIconFile);
106 	_deleteMedia->setIconOverride(&st::sendBoxAlbumGroupDeleteButtonIconFile);
107 
108 	updateFileRow(-1);
109 }
110 
updateFileRow(int row)111 void AlbumThumbnail::updateFileRow(int row) {
112 	if (row < 0) {
113 		_editMedia->hide();
114 		_deleteMedia->hide();
115 		return;
116 	}
117 	_editMedia->show();
118 	_deleteMedia->show();
119 
120 	const auto fileHeight = st::attachPreviewThumbLayout.thumbSize
121 		+ st::sendMediaRowSkip;
122 	const auto top = row * fileHeight + st::sendBoxFileGroupSkipTop;
123 
124 	auto right = st::sendBoxFileGroupSkipRight + st::boxPhotoPadding.right();
125 	_deleteMedia->moveToRight(right, top);
126 	right += st::sendBoxFileGroupEditInternalSkip + _deleteMedia->width();
127 	_editMedia->moveToRight(right, top);
128 }
129 
resetLayoutAnimation()130 void AlbumThumbnail::resetLayoutAnimation() {
131 	_animateFromGeometry = std::nullopt;
132 }
133 
animateLayoutToInitial()134 void AlbumThumbnail::animateLayoutToInitial() {
135 	_animateFromGeometry = countRealGeometry();
136 	_suggestedMove = 0.;
137 	_albumPosition = QPoint(0, 0);
138 }
139 
moveToLayout(const GroupMediaLayout & layout)140 void AlbumThumbnail::moveToLayout(const GroupMediaLayout &layout) {
141 	animateLayoutToInitial();
142 	_layout = layout;
143 
144 	const auto width = _layout.geometry.width();
145 	const auto height = _layout.geometry.height();
146 	_albumCorners = GetCornersFromSides(_layout.sides);
147 	using Option = Images::Option;
148 	const auto options = Option::Smooth
149 		| Option::RoundedLarge
150 		| ((_albumCorners & RectPart::TopLeft)
151 			? Option::RoundedTopLeft
152 			: Option::None)
153 		| ((_albumCorners & RectPart::TopRight)
154 			? Option::RoundedTopRight
155 			: Option::None)
156 		| ((_albumCorners & RectPart::BottomLeft)
157 			? Option::RoundedBottomLeft
158 			: Option::None)
159 		| ((_albumCorners & RectPart::BottomRight)
160 			? Option::RoundedBottomRight
161 			: Option::None);
162 	const auto pixSize = GetImageScaleSizeForGeometry(
163 		{ _fullPreview.width(), _fullPreview.height() },
164 		{ width, height });
165 	const auto pixWidth = pixSize.width() * style::DevicePixelRatio();
166 	const auto pixHeight = pixSize.height() * style::DevicePixelRatio();
167 
168 	_albumImage = PixmapFromImage(Images::prepare(
169 		_fullPreview,
170 		pixWidth,
171 		pixHeight,
172 		options,
173 		width,
174 		height));
175 }
176 
photoHeight() const177 int AlbumThumbnail::photoHeight() const {
178 	return _photo.height() / style::DevicePixelRatio();
179 }
180 
paintInAlbum(Painter & p,int left,int top,float64 shrinkProgress,float64 moveProgress)181 void AlbumThumbnail::paintInAlbum(
182 		Painter &p,
183 		int left,
184 		int top,
185 		float64 shrinkProgress,
186 		float64 moveProgress) {
187 	const auto shrink = anim::interpolate(0, _shrinkSize, shrinkProgress);
188 	_lastShrinkValue = shrink;
189 	const auto geometry = countCurrentGeometry(moveProgress);
190 	const auto x = left + geometry.x();
191 	const auto y = top + geometry.y();
192 	if (shrink > 0 || moveProgress < 1.) {
193 		const auto size = geometry.size();
194 		if (shrinkProgress < 1 && _albumCorners != RectPart::None) {
195 			prepareCache(size, shrink);
196 			p.drawImage(x, y, _albumCache);
197 		} else {
198 			const auto to = QRect({ x, y }, size).marginsRemoved(
199 				{ shrink, shrink, shrink, shrink }
200 			);
201 			drawSimpleFrame(p, to, size);
202 		}
203 	} else {
204 		p.drawPixmap(x, y, _albumImage);
205 	}
206 	if (_isVideo) {
207 		const auto innerSize = st::msgFileLayout.thumbSize;
208 		const auto inner = QRect(
209 			x + (geometry.width() - innerSize) / 2,
210 			y + (geometry.height() - innerSize) / 2,
211 			innerSize,
212 			innerSize);
213 		{
214 			PainterHighQualityEnabler hq(p);
215 			p.setPen(Qt::NoPen);
216 			p.setBrush(st::msgDateImgBg);
217 			p.drawEllipse(inner);
218 		}
219 		st::historyFileThumbPlay.paintInCenter(p, inner);
220 	}
221 
222 	_lastRectOfButtons = paintButtons(
223 		p,
224 		{ x, y },
225 		geometry.width(),
226 		shrinkProgress);
227 
228 	_lastRectOfModify = QRect(QPoint(x, y), geometry.size());
229 }
230 
prepareCache(QSize size,int shrink)231 void AlbumThumbnail::prepareCache(QSize size, int shrink) {
232 	const auto width = std::max(
233 		_layout.geometry.width(),
234 		_animateFromGeometry ? _animateFromGeometry->width() : 0);
235 	const auto height = std::max(
236 		_layout.geometry.height(),
237 		_animateFromGeometry ? _animateFromGeometry->height() : 0);
238 	const auto cacheSize = QSize(width, height) * style::DevicePixelRatio();
239 
240 	if (_albumCache.width() < cacheSize.width()
241 		|| _albumCache.height() < cacheSize.height()) {
242 		_albumCache = QImage(cacheSize, QImage::Format_ARGB32_Premultiplied);
243 		_albumCache.setDevicePixelRatio(style::DevicePixelRatio());
244 	}
245 	_albumCache.fill(Qt::transparent);
246 	{
247 		Painter p(&_albumCache);
248 		const auto to = QRect(QPoint(), size).marginsRemoved(
249 			{ shrink, shrink, shrink, shrink }
250 		);
251 		drawSimpleFrame(p, to, size);
252 	}
253 	Images::prepareRound(
254 		_albumCache,
255 		ImageRoundRadius::Large,
256 		_albumCorners,
257 		QRect(QPoint(), size * style::DevicePixelRatio()));
258 }
259 
drawSimpleFrame(Painter & p,QRect to,QSize size) const260 void AlbumThumbnail::drawSimpleFrame(Painter &p, QRect to, QSize size) const {
261 	const auto fullWidth = _fullPreview.width();
262 	const auto fullHeight = _fullPreview.height();
263 	const auto previewSize = GetImageScaleSizeForGeometry(
264 		{ fullWidth, fullHeight },
265 		{ size.width(), size.height() });
266 	const auto previewWidth = previewSize.width() * style::DevicePixelRatio();
267 	const auto previewHeight = previewSize.height() * style::DevicePixelRatio();
268 	const auto width = size.width() * style::DevicePixelRatio();
269 	const auto height = size.height() * style::DevicePixelRatio();
270 	const auto scaleWidth = to.width() / float64(width);
271 	const auto scaleHeight = to.height() / float64(height);
272 	const auto Round = [](float64 value) {
273 		return int(base::SafeRound(value));
274 	};
275 	const auto [from, fillBlack] = [&] {
276 		if (previewWidth < width && previewHeight < height) {
277 			const auto toWidth = Round(previewWidth * scaleWidth);
278 			const auto toHeight = Round(previewHeight * scaleHeight);
279 			return std::make_pair(
280 				QRect(0, 0, fullWidth, fullHeight),
281 				QMargins(
282 					(to.width() - toWidth) / 2,
283 					(to.height() - toHeight) / 2,
284 					to.width() - toWidth - (to.width() - toWidth) / 2,
285 					to.height() - toHeight - (to.height() - toHeight) / 2));
286 		} else if (previewWidth * height > previewHeight * width) {
287 			if (previewHeight >= height) {
288 				const auto takeWidth = previewWidth * height / previewHeight;
289 				const auto useWidth = fullWidth * width / takeWidth;
290 				return std::make_pair(
291 					QRect(
292 						(fullWidth - useWidth) / 2,
293 						0,
294 						useWidth,
295 						fullHeight),
296 					QMargins(0, 0, 0, 0));
297 			} else {
298 				const auto takeWidth = previewWidth;
299 				const auto useWidth = fullWidth * width / takeWidth;
300 				const auto toHeight = Round(previewHeight * scaleHeight);
301 				const auto toSkip = (to.height() - toHeight) / 2;
302 				return std::make_pair(
303 					QRect(
304 						(fullWidth - useWidth) / 2,
305 						0,
306 						useWidth,
307 						fullHeight),
308 					QMargins(
309 						0,
310 						toSkip,
311 						0,
312 						to.height() - toHeight - toSkip));
313 			}
314 		} else {
315 			if (previewWidth >= width) {
316 				const auto takeHeight = previewHeight * width / previewWidth;
317 				const auto useHeight = fullHeight * height / takeHeight;
318 				return std::make_pair(
319 					QRect(
320 						0,
321 						(fullHeight - useHeight) / 2,
322 						fullWidth,
323 						useHeight),
324 					QMargins(0, 0, 0, 0));
325 			} else {
326 				const auto takeHeight = previewHeight;
327 				const auto useHeight = fullHeight * height / takeHeight;
328 				const auto toWidth = Round(previewWidth * scaleWidth);
329 				const auto toSkip = (to.width() - toWidth) / 2;
330 				return std::make_pair(
331 					QRect(
332 						0,
333 						(fullHeight - useHeight) / 2,
334 						fullWidth,
335 						useHeight),
336 					QMargins(
337 						toSkip,
338 						0,
339 						to.width() - toWidth - toSkip,
340 						0));
341 			}
342 		}
343 	}();
344 
345 	p.drawImage(to.marginsRemoved(fillBlack), _fullPreview, from);
346 	if (fillBlack.top() > 0) {
347 		p.fillRect(to.x(), to.y(), to.width(), fillBlack.top(), st::imageBg);
348 	}
349 	if (fillBlack.bottom() > 0) {
350 		p.fillRect(
351 			to.x(),
352 			to.y() + to.height() - fillBlack.bottom(),
353 			to.width(),
354 			fillBlack.bottom(),
355 			st::imageBg);
356 	}
357 	if (fillBlack.left() > 0) {
358 		p.fillRect(
359 			to.x(),
360 			to.y() + fillBlack.top(),
361 			fillBlack.left(),
362 			to.height() - fillBlack.top() - fillBlack.bottom(),
363 			st::imageBg);
364 	}
365 	if (fillBlack.right() > 0) {
366 		p.fillRect(
367 			to.x() + to.width() - fillBlack.right(),
368 			to.y() + fillBlack.top(),
369 			fillBlack.right(),
370 			to.height() - fillBlack.top() - fillBlack.bottom(),
371 			st::imageBg);
372 	}
373 }
374 
paintPhoto(Painter & p,int left,int top,int outerWidth)375 void AlbumThumbnail::paintPhoto(Painter &p, int left, int top, int outerWidth) {
376 	const auto size = _photo.size() / style::DevicePixelRatio();
377 	p.drawPixmapLeft(
378 		left + (st::sendMediaPreviewSize - size.width()) / 2,
379 		top,
380 		outerWidth,
381 		_photo);
382 
383 	const auto topLeft = QPoint{ left, top };
384 
385 	_lastRectOfButtons = paintButtons(
386 		p,
387 		topLeft,
388 		st::sendMediaPreviewSize,
389 		0);
390 
391 	_lastRectOfModify = QRect(topLeft, size);
392 }
393 
paintFile(Painter & p,int left,int top,int outerWidth)394 void AlbumThumbnail::paintFile(
395 		Painter &p,
396 		int left,
397 		int top,
398 		int outerWidth) {
399 	const auto &st = st::attachPreviewThumbLayout;
400 	const auto textLeft = left + st.thumbSize + st.padding.right();
401 
402 	p.drawPixmap(left, top, _fileThumb);
403 	p.setFont(st::semiboldFont);
404 	p.setPen(st::historyFileNameInFg);
405 	p.drawTextLeft(
406 		textLeft,
407 		top + st.nameTop,
408 		outerWidth,
409 		_name,
410 		_nameWidth);
411 	p.setFont(st::normalFont);
412 	p.setPen(st::mediaInFg);
413 	p.drawTextLeft(
414 		textLeft,
415 		top + st.statusTop,
416 		outerWidth,
417 		_status,
418 		_statusWidth);
419 
420 	_lastRectOfModify = QRect(
421 		QPoint(left, top),
422 		_fileThumb.size() / style::DevicePixelRatio());
423 }
424 
containsPoint(QPoint position) const425 bool AlbumThumbnail::containsPoint(QPoint position) const {
426 	return _layout.geometry.contains(position);
427 }
428 
buttonsContainPoint(QPoint position) const429 bool AlbumThumbnail::buttonsContainPoint(QPoint position) const {
430 	return (_isPhoto
431 		? _lastRectOfModify
432 		: _lastRectOfButtons).contains(position);
433 }
434 
buttonTypeFromPoint(QPoint position) const435 AttachButtonType AlbumThumbnail::buttonTypeFromPoint(QPoint position) const {
436 	if (!buttonsContainPoint(position)) {
437 		return AttachButtonType::None;
438 	}
439 	return !_lastRectOfButtons.contains(position)
440 		? AttachButtonType::Modify
441 		: (position.x() < _lastRectOfButtons.center().x())
442 		? AttachButtonType::Edit
443 		: AttachButtonType::Delete;
444 }
445 
distanceTo(QPoint position) const446 int AlbumThumbnail::distanceTo(QPoint position) const {
447 	const auto delta = (_layout.geometry.center() - position);
448 	return QPoint::dotProduct(delta, delta);
449 }
450 
isPointAfter(QPoint position) const451 bool AlbumThumbnail::isPointAfter(QPoint position) const {
452 	return position.x() > _layout.geometry.center().x();
453 }
454 
moveInAlbum(QPoint to)455 void AlbumThumbnail::moveInAlbum(QPoint to) {
456 	_albumPosition = to;
457 }
458 
center() const459 QPoint AlbumThumbnail::center() const {
460 	auto realGeometry = _layout.geometry;
461 	realGeometry.moveTopLeft(realGeometry.topLeft() + _albumPosition);
462 	return realGeometry.center();
463 }
464 
suggestMove(float64 delta,Fn<void ()> callback)465 void AlbumThumbnail::suggestMove(float64 delta, Fn<void()> callback) {
466 	if (_suggestedMove != delta) {
467 		_suggestedMoveAnimation.start(
468 			std::move(callback),
469 			_suggestedMove,
470 			delta,
471 			kShrinkDuration);
472 		_suggestedMove = delta;
473 	}
474 }
475 
countRealGeometry() const476 QRect AlbumThumbnail::countRealGeometry() const {
477 	const auto addLeft = int(base::SafeRound(
478 		_suggestedMoveAnimation.value(_suggestedMove) * _lastShrinkValue));
479 	const auto current = _layout.geometry;
480 	const auto realTopLeft = current.topLeft()
481 		+ _albumPosition
482 		+ QPoint(addLeft, 0);
483 	return { realTopLeft, current.size() };
484 }
485 
countCurrentGeometry(float64 progress) const486 QRect AlbumThumbnail::countCurrentGeometry(float64 progress) const {
487 	const auto now = countRealGeometry();
488 	if (_animateFromGeometry && progress < 1.) {
489 		return {
490 			anim::interpolate(_animateFromGeometry->x(), now.x(), progress),
491 			anim::interpolate(_animateFromGeometry->y(), now.y(), progress),
492 			anim::interpolate(_animateFromGeometry->width(), now.width(), progress),
493 			anim::interpolate(_animateFromGeometry->height(), now.height(), progress)
494 		};
495 	}
496 	return now;
497 }
498 
finishAnimations()499 void AlbumThumbnail::finishAnimations() {
500 	_suggestedMoveAnimation.stop();
501 }
502 
paintButtons(Painter & p,QPoint point,int outerWidth,float64 shrinkProgress)503 QRect AlbumThumbnail::paintButtons(
504 		Painter &p,
505 		QPoint point,
506 		int outerWidth,
507 		float64 shrinkProgress) {
508 	const auto &skipRight = st::sendBoxAlbumGroupSkipRight;
509 	const auto &skipTop = st::sendBoxAlbumGroupSkipTop;
510 	const auto groupWidth = _buttons.width();
511 
512 	// If the width is tiny, it would be better to not display the buttons.
513 	if (groupWidth > outerWidth) {
514 		return QRect();
515 	}
516 
517 	// If the width is too small,
518 	// it would be better to display the buttons in the center.
519 	const auto groupX = point.x() + ((groupWidth + skipRight * 2 > outerWidth)
520 		? (outerWidth - groupWidth) / 2
521 		: outerWidth - skipRight - groupWidth);
522 	const auto groupY = point.y() + skipTop;
523 
524 	const auto opacity = p.opacity();
525 	p.setOpacity(1.0 - shrinkProgress);
526 	_buttons.paint(p, groupX, groupY);
527 	p.setOpacity(opacity);
528 
529 	return QRect(groupX, groupY, groupWidth, _buttons.height());
530 }
531 
532 } // namespace Ui
533