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